Evidensmotor (ECM v1.0): ingen slutsats utan underlag

Regelmotorn kodar plattformens viktigaste princip: systemet får aldrig
anta att en kontroll är utförd eller att dokumentation finns. Varje
påstående måste kunna härledas till evidens i händelseloggen.

- Nytt versionshanterat regelbibliotek (src/felsokning/ecm.ts, ECM v1.0):
  evidensnivåer E0–E6 härledda ur loggen, fullbordansregler och
  kvalitetsgrind — skilt från applikationslogiken
- Fullbordansregel i guiden: en kontroll slutförs med evidens ELLER
  uttryckligt undantag "Underlag kan inte tas fram" med obligatorisk
  orsak — loggas och flaggas ⚠ i brief, överlämning och rapport
- Kvalitetsgrind före slutrapport: utskriften spärrad tills
  objektidentifiering, metodikens kontroller, fotokrav och evidensnivå
  är gröna; varje röd rad visar exakt vad som saknas
- Orkesterns grundprompt utökad: aldrig "OK/kontrollerad/inga fel" utan
  evidens — skriv "Evidens saknas" och begär rätt underlag (foto/video/
  mätvärde/skärmfoto)
- Visual-first instrumentavläsning: ny vision-uppgift läser multimetrar,
  diagnosskärmar m.m. — värden/enheter/felkoder med konfidens, teknikern
  bekräftar, originalbilden loggas alltid bredvid strukturerad data;
  kameran är integrationslagret, inga verktygsintegrationer krävs
- Terminologi: "AI" ersatt med systemspråk i hela gränssnittet
  (Beslutsstöd, Systemet analyserar, Granskning av underlaget …)
- Dokumentation: docs/moduler/evidensmotor.md; 41 vitest-tester

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 09:05:04 +00:00
parent a197ee10ca
commit e8cf6db236
12 changed files with 761 additions and 32 deletions
+5 -1
View File
@@ -34,7 +34,9 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
| Dashboard | ✅ Enligt direktivet: räknare och filter för Alla/Pågående/Klara plus Starta nytt ärende. |
| Ärendestart via arbetsorder | ✅ Primärvägen när ett ärende startas: fota arbetsorderns framsida — orkesterns dokumenttolkning (Claude Sonnet 5, vision) läser kund-, fordons- och verkstadsuppgifter oavsett layout och sätter konfidens per fält. 🟢 ≥95 % godkänns automatiskt, 🟡 8095 % markeras för genomläsning, 🔴 <80 % kräver aktiv bekräftelse — teknikern granskar bara osäkra fält. Visuell granskning med dokumentet bredvid fälten (klick markerar ungefärlig position), sedan skapas hela ärendet med ett tryck. Tolkningen loggas som organisationsintern händelse (`arbetsorder_skannad`) och delas aldrig i kund-/partnervyer. Manuell inmatning finns kvar som andrahandsväg; i lokalt läge visas en tydligt märkt demo-tolkning. Inloggade användare tillfrågas aldrig om namn — kontot vet redan. |
| Inställningar | ✅ Systemadministratören väljer vilka objekttyper och identifieringsmetoder som visas när ett ärende startas (`/felsokning/installningar`). På plattformen gäller valet hela organisationen (sparas på organisationen, endast admin får ändra — verifierat i integrationstestet); i lokalt läge gäller valet enheten. Okända värden filtreras och tomma listor faller tillbaka till standard. |
| Utskrift | ✅ Kundrapport och Live Share-vy skrivs ut svart på vitt; interaktiva element döljs automatiskt. |
| Evidensmotor (ECM) | ✅ Versionshanterat regelbibliotek ([moduler/evidensmotor.md](moduler/evidensmotor.md), `src/felsokning/ecm.ts`, ECM v1.0): evidensnivåer E0E6 härledda ur loggen, fullbordansregeln *evidens eller dokumenterat undantag med obligatorisk orsak* ("Underlag kan inte tas fram" i guiden, flaggas ⚠ i brief/rapport), och **kvalitetsgrind före slutrapport** — utskrift spärrad tills objektidentifiering, kontroller, fotokrav och evidensnivå är gröna. Regeln "skriv aldrig OK/kontrollerad/inga fel utan evidens — skriv Evidens saknas" är kodad i orkesterns grundprompt. |
| Instrumentavläsning (visual-first) | ✅ Kameran som universellt gränssnitt: `📷 Instrument` i Dokumentera-panelen fotograferar multimetrar, diagnosskärmar, batteritestare m.m. — bildtolkningen identifierar instrumenttyp och extraherar värden/enheter/felkoder med konfidens per värde; teknikern bekräftar innan något loggas. Originalbilden loggas alltid tillsammans med de strukturerade mätvärdena — strukturerad data ersätter aldrig originalevidensen. Ingen integration mot diagnossystem krävs. |
| Utskrift | ✅ Kundrapport och Live Share-vy skrivs ut svart på vitt; interaktiva element döljs automatiskt. Utskriften går genom ECM-kvalitetsgrinden. |
| Öppet API | ✅ Plattforms-API:t är dokumenterat med OpenAPI 3.0 (`services/plattform/openapi.yaml`) — auth, användare, ärenden/händelser (append-only), översikt, publik delning och AI-orkestern, med scheman för alla händelsetyper. Specen valideras maskinellt, paritetstestas mot serverns rutter och serveras live på `GET /api/openapi.yaml`. |
## Arkitekturprinciper i koden
@@ -42,6 +44,8 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st
- **Händelseloggen är enda sanningskällan.** `src/felsokning/domain.ts` definierar händelsetyperna; poster läggs endast till.
- **Alla vyer är projektioner.** `src/felsokning/projektioner.ts` — brief, tidsfördelning, överlämningstext och kundrapport är rena funktioner av loggen och kan alltid regenereras. Testerna i `src/felsokning/__tests__/` låser detta.
- **Metodikmotorn är deterministisk.** `src/felsokning/metodik.ts` — nästa steg härleds ur vad som redan dokumenterats. Det är här den framtida AI:n ansluter, utan att logg eller projektioner ändras.
- **Ingen slutsats utan evidens.** `src/felsokning/ecm.ts` — regelmotorn (ECM) validerar varje påstående mot händelseloggen: fullbordansregler, evidensnivåer och kvalitetsgrind. Kameran är integrationslagret (visual-first) — det som syns på en skärm eller ett instrument fotograferas och tolkas i stället för att integreras.
- **Terminologi.** Produkten beskrivs som ett evidensbaserat diagnossystem/intelligent beslutsstöd — i UI och kundkommunikation används *systemet/analysen/bedömningen/beslutsstödet*, aldrig "AI" om det inte är tekniskt nödvändigt.
- **Industriellt verkstads-UI (ETKA-inspirerat).** `src/felsokning/ui.tsx` — plana ljusgrå ytor (#ECECEC/#F7F7F7), skarpa kanter, djup marinblå som primärfärg, tät typografi (1115 px), rektangulära knappar (max 4 px radie), verktygsrad ~44 px. Ärendesidan har klassisk trekolumnslayout på skrivbord: navigationsträd (vyer + metodikstegens status) till vänster, arbetsyta i mitten, kontextpanel (teknisk information, tillförlitlighet, teknisk rekommendation) till höger; en kolumn med flikrad på smala skärmar.
## Medvetna avgränsningar
+108
View File
@@ -0,0 +1,108 @@
# Modul: Evidensmotorn (ECM — Evidence & Compliance Matrix)
**Version: ECM v1.0** · Regelbiblioteket är versionshanterat och skilt från
applikationslogiken (`src/felsokning/ecm.ts`). Vyerna anropar bara motorns
rena funktioner — regler kan uppdateras utan att applikationen byggs om.
## Grundprincip
**Inget underlag = ingen slutsats.**
Systemet får aldrig anta att en kontroll är utförd eller påstå att
dokumentation finns om den inte faktiskt är insamlad. Varje påstående i
diagnos, brief och slutrapport ska kunna härledas till minst en
evidenspost i händelseloggen. Systemet skriver aldrig "OK",
"kontrollerad", "inga fel" eller "åtgärdad" utan evidens — i stället:
**"Evidens saknas."** Regeln är kodad i orkesterns grundprompt och kan
inte kringgås från klienten.
## Evidensnivåer
| Nivå | Typ | Bevisvärde |
| --- | --- | --- |
| E0 | Inget underlag | 0 % |
| E1 | Teknikerns observation | Lågt |
| E2 | Foto | Medel |
| E3 | Video | Högt |
| E4 | Mätvärde | Högt |
| E5 | Diagnosdata/dokument (t.ex. skannad arbetsorder) | Mycket högt |
| E6 | Flera oberoende källor | Högsta |
Ärendets nivå härleds ur loggens faktiska innehåll (`evidensNiva`) och
visas i kvalitetsgrinden.
## Fullbordansregler
En kontrollpunkt kan bara slutföras när något av följande är sant:
1. **Krävd evidens är insamlad** — foto för det synliga, mätvärde för det
som mäts, kommentar där inget bättre är möjligt (metodikens
`krav`-fält per kontroll).
2. **Teknikern dokumenterar ett undantag**: *"Underlag kan inte tas
fram"* med **obligatorisk orsak** (komponenten oåtkomlig, fordonet kan
inte lyftas säkert, kunden avböjde demontering, dålig sikt, utrustning
saknas — eller fri text). Undantaget loggas i händelseloggen och
flaggas ⚠ i brief, överlämning och rapport — det redovisas aldrig som
"utförd".
## Kvalitetsgrind före slutrapport
Slutrapporten kan inte genereras förrän grinden är godkänd
(`kvalitetsgrind`/`grindGodkand`):
| Kontroll | Krav |
| --- | --- |
| Fordons-/objektidentifiering verifierad | Obligatorisk |
| Arbetsorder inläst | Rekommenderas |
| Metodikens kontroller: evidens eller dokumenterat undantag | Obligatorisk |
| Foton finns för fotokrävande kontroller | Obligatorisk |
| Hypoteser redovisas som ej verifierade | Informativ (alltid sant per konstruktion) |
| Evidensnivå över E0 | Obligatorisk |
Utskriftsknappen är spärrad tills varje obligatorisk rad är grön; varje
röd rad visar exakt vad som saknas.
## Visual-first: kameran är integrationslagret
Plattformen prioriterar visuell insamling framför systemintegrationer.
När information redan visas på en skärm, ett instrument, en utskrift
eller en etikett fotograferas den — bildtolkningen extraherar, validerar
och strukturerar informationen automatiskt. Ingen specialintegration
behövs mot Bosch, TEXA, Autel, Launch, Hella Gutmann m.fl. så länge en
människa kan läsa informationen.
- **Arbetsorder** → ärendestartens dokumenttolkning (fält + konfidens).
- **Instrument/diagnosskärmar** → `📷 Instrument` i Dokumentera-panelen:
foto → typidentifiering (multimeter, diagnosdator, batteritestare,
mätarkluster, manometer …) → värden med enhet och konfidens → teknikern
bekräftar → **originalbilden loggas alltid tillsammans med de
strukturerade värdena** — strukturerad data ersätter aldrig
originalevidensen.
Värden med hög säkerhet godkänns automatiskt; osäkra markeras för
granskning — samma konfidensmodell som arbetsorderskanningen
(🟢 ≥95 %, 🟡 8095 %, 🔴 <80 %).
## Spårbarhet
Varje evidenspost är en händelse i den append-only-loggen med tidpunkt,
tekniker, ärende och innehåll — omöjlig att ändra i efterhand (triggers i
databasen). Det ger varje slutsats juridiskt spårbart underlag för kund,
försäkringsbolag eller domstol.
## Terminologi
Produkten beskrivs aldrig som en "AI-app" utan som ett **evidensbaserat
diagnossystem** / **intelligent beslutsstöd**. I användargränssnitt och
dokument används *systemet, analysen, bedömningen, tolkningen,
bildtolkningen, beslutsstödet, regelmotorn* — inte "AI", om det inte är
tekniskt nödvändigt (t.ex. i arkitekturdokumentation om modellorkestern).
## Kommande (regelbibliotekets väg framåt)
- Områdesregler per komponent (däck: fyra bilder + DOT + dimension;
bromsar: närbild per ok; motorljud: video med ljud …) som
serverdistribuerade, versionerade regelpaket.
- Video- och ljudevidens (E3) med analys.
- Garanti-/försäkrings-/reklamationsprofiler med egna obligatoriska
fält i kvalitetsgrinden.
+44
View File
@@ -32,6 +32,7 @@ Absoluta regler:
- "hypotes": möjlig felorsak som KRÄVER verifiering — formulera alltid vad som skulle verifiera den.
- "rekommendation": nästa verifierbara kontroll eller mätning.
- Om underlaget är otillräckligt: säg det uttryckligen i en observation och rekommendera vad som behöver dokumenteras.
- Skriv aldrig "OK", "kontrollerad", "inga fel" eller "åtgärdad" om evidens saknas i underlaget — skriv i stället "Evidens saknas" och begär rätt underlag: foto för det synliga, video med ljud för det som låter, video för det som rör sig, mätvärde för det som mäts, foto av skärmen när ett instrument eller en diagnosdator visar informationen.
- Svara på svenska, konsekvent, kortfattat och metodiskt. "nastaSteg" är EN konkret, verifierbar åtgärd.`;
const SVARS_SCHEMA = {
@@ -116,6 +117,40 @@ Regler:
- Normalisera: registreringsnummer i versaler utan mellanslag, VIN med 17 tecken, datum som ÅÅÅÅ-MM-DD, mätarställning med enhet.
- "felbeskrivning" är kundens beskrivna problem/arbetsbegäran om en sådan finns i dokumentet.`;
// Visual-first: instrument och diagnosskärmar fotograferas i stället för
// att integreras — kameran är det universella gränssnittet.
const INSTRUMENT_SCHEMA = {
type: "object",
properties: {
instrumenttyp: { type: "string" },
varden: {
type: "array",
items: {
type: "object",
properties: {
beskrivning: { type: "string" },
varde: { type: "string" },
enhet: { type: "string" },
konfidens: { type: "number", minimum: 0, maximum: 1 },
},
required: ["beskrivning", "varde", "konfidens"],
additionalProperties: false,
},
},
},
required: ["instrumenttyp", "varden"],
additionalProperties: false,
};
const INSTRUMENT_REGLER = `Du läser av ett foto av ett instrument eller en skärm i en fordonsverkstad: multimeter, diagnosdator, oscilloskop, batteritestare, mätarkluster, manometer, utskrift eller etikett.
Regler:
- Identifiera instrumenttypen och extrahera ENDAST värden som faktiskt går att läsa i bilden. Hitta aldrig på värden.
- Varje värde får en beskrivning (t.ex. "Batterispänning", "Felkod P0301 misständning cylinder 1"), värdet som text och enhet när den syns (V, A, Ω, bar, °C, rpm …).
- Felkoder returneras som egna värden med kod och klartext om skärmen visar den.
- "konfidens" är din läs-säkerhet 01; sänk vid oskarp bild, reflexer eller delvis skymda siffror.`;
// Orkestern: en modell per uppgiftstyp (samma routing som edge-funktionen).
const ORKESTER = {
handledning: {
@@ -151,6 +186,15 @@ Gissa inte: välj "generisk" om beskrivningen inte tydligt hör till en specifik
},
// Ärendestart: teknikern fotograferar arbetsordern; vi läser dokumentet
// och returnerar strukturerade fält med konfidens per värde.
// Visual-first: foto av instrument/diagnosskärm → strukturerade värden.
instrumentavlasning: {
modell: "claude-sonnet-5",
effort: "low",
maxTokens: 1024,
system: INSTRUMENT_REGLER,
schema: INSTRUMENT_SCHEMA,
bild: true,
},
dokumenttolkning: {
modell: "claude-sonnet-5",
effort: "low",
+108
View File
@@ -0,0 +1,108 @@
import { describe, expect, it } from "vitest";
import type { Arende, Handelse } from "../domain";
import { nyLoggPost } from "../domain";
import { VIBRATION_METODIK } from "../metodik";
import { evidensNiva, grindGodkand, kvalitetsgrind } from "../ecm";
import { byggDemoArende } from "../demo";
function byggArende(handelser: Handelse[]): Arende {
return {
id: "ecm-test",
nummer: 1,
skapad: "2026-08-03T08:00:00Z",
handelser: handelser.map((h) => nyLoggPost("Anna", h, "2026-08-03T08:00:00Z")),
};
}
const OBJEKT: Handelse = {
typ: "objekt_identifierat",
objekt: { typ: "Personbil", identifierare: "ABC123", identifieringsmetod: "Regnr", beskrivning: "Volvo XC60" },
};
describe("evidensnivåer", () => {
it("härleder nivån ur loggens faktiska innehåll", () => {
expect(evidensNiva(byggArende([]))).toBe("E0");
expect(evidensNiva(byggArende([{ typ: "observation", text: "x" }]))).toBe("E1");
expect(evidensNiva(byggArende([{ typ: "foto", beskrivning: "x", dataUrl: "data:" }]))).toBe("E2");
expect(evidensNiva(byggArende([{ typ: "matvarde", beskrivning: "U", varde: "12" }]))).toBe("E4");
expect(evidensNiva(byggArende([{ typ: "arbetsorder_skannad", falt: [] }]))).toBe("E5");
expect(
evidensNiva(
byggArende([
{ typ: "foto", beskrivning: "x", dataUrl: "data:" },
{ typ: "matvarde", beskrivning: "U", varde: "12" },
]),
),
).toBe("E6");
});
});
describe("kvalitetsgrind före slutrapport", () => {
it("utan evidens är grinden stängd — inga påståenden utan underlag", () => {
const tomt = byggArende([]);
expect(grindGodkand(tomt, VIBRATION_METODIK)).toBe(false);
const rader = kvalitetsgrind(tomt, VIBRATION_METODIK);
expect(rader.find((r) => r.id === "objekt")?.ok).toBe(false);
expect(rader.find((r) => r.id === "kontroller")?.ok).toBe(false);
expect(rader.find((r) => r.id === "kontroller")?.detalj).toContain("Evidens saknas");
});
it("ett dokumenterat undantag räknas som hanterat men flaggas med orsak", () => {
// Alla kontroller får undantag med orsak — grinden öppnas, men
// undantagen redovisas i detaljraden (aldrig som "utförd").
const undantag: Handelse[] = [OBJEKT, { typ: "foto", beskrivning: "Översikt", dataUrl: "data:" }, { typ: "matvarde", beskrivning: "Lufttryck", varde: "2,4", enhet: "bar" }];
for (const steg of VIBRATION_METODIK.steg) {
for (const kontroll of steg.kontroller ?? []) {
undantag.push({
typ: "kontroll_utford",
stegId: steg.id,
kontrollId: kontroll.id,
text: kontroll.text,
undantag: "Kunden avböjde demontering",
});
}
}
const arende = byggArende(undantag);
expect(grindGodkand(arende, VIBRATION_METODIK)).toBe(true);
const rad = kvalitetsgrind(arende, VIBRATION_METODIK).find((r) => r.id === "kontroller");
expect(rad?.ok).toBe(true);
expect(rad?.detalj).toContain("Undantag med orsak");
expect(rad?.detalj).toContain("Kunden avböjde demontering");
});
it("demoärendet blockeras tills alla kontroller har evidens eller undantag", () => {
const demo = byggDemoArende(1);
expect(grindGodkand(demo, VIBRATION_METODIK)).toBe(false);
const komplett: Arende = {
...demo,
handelser: [...demo.handelser],
};
const hanterade = new Set(
demo.handelser
.filter((p) => p.handelse.typ === "kontroll_utford")
.map((p) => {
const h = p.handelse;
return h.typ === "kontroll_utford" ? `${h.stegId}/${h.kontrollId}` : "";
}),
);
for (const steg of VIBRATION_METODIK.steg) {
for (const kontroll of steg.kontroller ?? []) {
if (!hanterade.has(`${steg.id}/${kontroll.id}`)) {
komplett.handelser.push(
nyLoggPost("Johan", {
typ: "kontroll_utford",
stegId: steg.id,
kontrollId: kontroll.id,
text: kontroll.text,
undantag: "Utrustning saknas",
}),
);
}
}
}
expect(grindGodkand(komplett, VIBRATION_METODIK)).toBe(true);
// Demoärendet har foto + mätvärden + flera källor → högsta evidensnivån.
expect(evidensNiva(komplett)).toBe("E6");
});
});
+54 -1
View File
@@ -21,7 +21,13 @@ import { METODIKER } from "./metodik";
import type { Brief } from "./projektioner";
import { brief } from "./projektioner";
export type AiUppgift = "handledning" | "granskning" | "sammanfattning" | "metodikval" | "dokumenttolkning";
export type AiUppgift =
| "handledning"
| "granskning"
| "sammanfattning"
| "metodikval"
| "dokumenttolkning"
| "instrumentavlasning";
export type AiRadTyp = "observation" | "verifierat" | "hypotes" | "rekommendation";
@@ -159,6 +165,53 @@ export async function tolkaArbetsorder(
return resultat ? { falt: normaliseraArbetsorder(resultat.svar), modell: resultat.modell } : null;
}
// ---- Instrumentavläsning (visual-first) -------------------------------
// Kameran är det universella gränssnittet: när ett instrument, en
// diagnosdator eller ett dokument visar information fotograferas det —
// systemet extraherar värden, enheter och felkoder med konfidens per
// värde. Originalbilden bevaras alltid som evidens.
export interface InstrumentVarde {
beskrivning: string;
varde: string;
enhet?: string;
konfidens: number; // 01
}
export interface InstrumentTolkning {
instrumenttyp: string;
varden: InstrumentVarde[];
}
export function normaliseraInstrument(data: unknown): InstrumentTolkning {
const d = data as Partial<InstrumentTolkning>;
if (!d || typeof d.instrumenttyp !== "string" || !Array.isArray(d.varden)) {
throw new Error("Oväntat avläsningsformat");
}
const varden: InstrumentVarde[] = [];
for (const rad of d.varden as Partial<InstrumentVarde>[]) {
if (typeof rad?.beskrivning !== "string" || typeof rad.varde !== "string" || !rad.varde.trim()) continue;
varden.push({
beskrivning: rad.beskrivning.trim(),
varde: rad.varde.trim(),
enhet: typeof rad.enhet === "string" && rad.enhet.trim() ? rad.enhet.trim() : undefined,
konfidens: typeof rad.konfidens === "number" ? Math.min(1, Math.max(0, rad.konfidens)) : 0,
});
}
return { instrumenttyp: d.instrumenttyp.trim() || "instrument", varden };
}
export async function lasAvInstrument(
dataUrl: string,
): Promise<{ tolkning: InstrumentTolkning; modell: string } | null> {
const resultat = await anropa(
"instrumentavlasning",
"Läs av instrumentet/skärmen i den bifogade bilden.",
{ bild: dataUrl },
);
return resultat ? { tolkning: normaliseraInstrument(resultat.svar), modell: resultat.modell } : null;
}
// I Kubernetes-driften pekar VITE_AI_ORKESTER_URL på orkestertjänsten
// (services/ai-orkester); utan den används Supabase-edge-funktionen.
const ORKESTER_URL = (import.meta.env.VITE_AI_ORKESTER_URL as string | undefined)?.replace(/\/$/, "");
+13 -1
View File
@@ -4,7 +4,7 @@
// har verkligt innehåll direkt.
import type { Arende, Handelse, LoggPost } from "./domain";
import type { TolkatFalt } from "./ai";
import type { InstrumentTolkning, TolkatFalt } from "./ai";
import { nyLoggPost } from "./domain";
// Enkla platshållarbilder ritade i canvas — ersätts av riktiga foton så
@@ -139,3 +139,15 @@ export function byggDemoTolkning(): TolkatFalt[] {
{ id: "felbeskrivning", etikett: "Felbeskrivning", grupp: "Ärende", varde: "Kunden upplever vibrationer i ratten vid ca 90 km/h", konfidens: 0.97, omrade: { x: 0.08, y: 0.45, bredd: 0.84, hojd: 0.07 } },
];
}
// Demo-avläsning av ett instrument — används i lokalt läge där
// bildtolkningen inte kan nås. Märks alltid som demo i UI:t.
export function byggDemoInstrument(): InstrumentTolkning {
return {
instrumenttyp: "multimeter",
varden: [
{ beskrivning: "Batterispänning (vila)", varde: "12,4", enhet: "V", konfidens: 0.97 },
{ beskrivning: "Spänning vid start", varde: "9,8", enhet: "V", konfidens: 0.84 },
],
};
}
+4 -1
View File
@@ -51,7 +51,9 @@ export type Handelse =
| { typ: "arbetsorder_skannad"; falt: ArbetsorderFalt[]; dataUrl?: string }
| { typ: "felbeskrivning"; text: string }
| { typ: "fraga_besvarad"; stegId: string; frageId: string; fraga: string; svar: string }
| { typ: "kontroll_utford"; stegId: string; kontrollId: string; text: string; resultat?: string }
// undantag: underlaget kunde inte tas fram — obligatorisk orsak i stället
// för evidens. Kontrollen räknas som hanterad men flaggas i brief/rapport.
| { typ: "kontroll_utford"; stegId: string; kontrollId: string; text: string; resultat?: string; undantag?: string }
| { typ: "observation"; text: string }
| { typ: "matvarde"; beskrivning: string; varde: string; enhet?: string }
| { typ: "hypotes"; text: string; niva: Exclude<Tillforlitlighet, "hog"> }
@@ -114,6 +116,7 @@ export function handelseRubrik(post: LoggPost): string {
case "fraga_besvarad":
return `${h.fraga}${h.svar}`;
case "kontroll_utford":
if (h.undantag) return `${h.text} — underlag kunde inte tas fram: ${h.undantag}`;
return h.resultat ? `${h.text}${h.resultat}` : `${h.text} — utförd`;
case "observation":
return `Observation: ${h.text}`;
+161
View File
@@ -0,0 +1,161 @@
// Evidence & Compliance Matrix (ECM) — plattformens regelmotor.
//
// Grundprincip: varje påstående måste kunna härledas till evidens i
// händelseloggen. Systemet får aldrig skriva "kontrollerad", "OK" eller
// "inga fel" utan underlag — utan evidens skrivs "Evidens saknas".
//
// Regelbiblioteket är versionshanterat och skilt från applikationslogiken:
// evidensnivåer, krav och kvalitetsgrind ändras här (och i kommande
// versioner via serverdistribuerade regler) utan att vyerna byggs om.
// Vyerna anropar bara de rena funktionerna nedan.
import type { Arende } from "./domain";
import type { Metodik } from "./metodik";
export const ECM_VERSION = "1.0";
// Evidensnivåer: bevisvärdet för ett påstående, härlett ur loggen.
// E0 inget underlag · E1 teknikerns observation · E2 foto · E3 video
// E4 mätvärde · E5 diagnosdata/dokument · E6 flera oberoende källor
export type EvidensNiva = "E0" | "E1" | "E2" | "E3" | "E4" | "E5" | "E6";
export const EVIDENS_LABEL: Record<EvidensNiva, string> = {
E0: "E0 · Inget underlag",
E1: "E1 · Teknikerns observation",
E2: "E2 · Foto",
E3: "E3 · Video",
E4: "E4 · Mätvärde",
E5: "E5 · Diagnosdata/dokument",
E6: "E6 · Flera oberoende källor",
};
// Godkända orsaker när underlag inte kan tas fram. Fri text tillåts
// också — men en orsak är alltid obligatorisk.
export const UNDANTAGSORSAKER = [
"Komponenten är oåtkomlig",
"Fordonet kan inte lyftas säkert",
"Kunden avböjde demontering",
"Dålig sikt/åtkomst",
"Utrustning saknas",
];
// Evidensnivå för ärendet som helhet: den starkaste kombination som
// loggen faktiskt innehåller.
export function evidensNiva(arende: Arende): EvidensNiva {
let foto = false;
let matvarde = false;
let dokument = false;
let observation = false;
for (const post of arende.handelser) {
const h = post.handelse;
if (h.typ === "foto") foto = true;
if (h.typ === "matvarde") matvarde = true;
if (h.typ === "arbetsorder_skannad") dokument = true;
if (h.typ === "observation" || h.typ === "kontroll_utford") observation = true;
}
const kallor = [foto, matvarde, dokument].filter(Boolean).length;
if (kallor >= 2) return "E6";
if (dokument) return "E5";
if (matvarde) return "E4";
if (foto) return "E2";
if (observation) return "E1";
return "E0";
}
export interface GrindRad {
id: string;
rubrik: string;
ok: boolean;
// Obligatorisk: blockerar slutrapporten tills den är grön.
kravs: boolean;
detalj?: string;
}
// Kvalitetsgrind före slutrapport: varje rad är en verifierbar kontroll
// mot händelseloggen. Rapporten kan inte genereras förrän alla
// obligatoriska rader är gröna — evidens eller dokumenterat undantag.
export function kvalitetsgrind(arende: Arende, metodik: Metodik): GrindRad[] {
const rader: GrindRad[] = [];
const handelser = arende.handelser.map((p) => p.handelse);
const objektFinns = handelser.some((h) => h.typ === "objekt_identifierat");
rader.push({
id: "objekt",
rubrik: "Fordons-/objektidentifiering verifierad",
ok: objektFinns,
kravs: true,
detalj: objektFinns ? undefined : "Evidens saknas — identifiera objektet.",
});
const arbetsorder = handelser.some((h) => h.typ === "arbetsorder_skannad");
rader.push({
id: "arbetsorder",
rubrik: "Arbetsorder inläst",
ok: arbetsorder,
kravs: false,
detalj: arbetsorder ? undefined : "Ärendet startades utan skannad arbetsorder.",
});
// Metodikens kontroller: evidens eller dokumenterat undantag per kontroll.
const hanterade = new Map<string, { undantag?: string }>();
for (const h of handelser) {
if (h.typ === "kontroll_utford") hanterade.set(`${h.stegId}/${h.kontrollId}`, { undantag: h.undantag });
}
const fotonFinns = handelser.filter((h) => h.typ === "foto").length;
const saknade: string[] = [];
const undantagna: string[] = [];
let fotoKravUtanFoto = 0;
for (const steg of metodik.steg) {
for (const kontroll of steg.kontroller ?? []) {
const status = hanterade.get(`${steg.id}/${kontroll.id}`);
if (!status) saknade.push(kontroll.text);
else if (status.undantag) undantagna.push(`${kontroll.text} (${status.undantag})`);
else if (kontroll.krav === "foto" && fotonFinns === 0) fotoKravUtanFoto += 1;
}
}
rader.push({
id: "kontroller",
rubrik: "Metodikens kontroller: evidens eller dokumenterat undantag",
ok: saknade.length === 0,
kravs: true,
detalj:
saknade.length > 0
? `Evidens saknas: ${saknade.join("; ")}`
: undantagna.length > 0
? `Undantag med orsak: ${undantagna.join("; ")}`
: undefined,
});
rader.push({
id: "fotokrav",
rubrik: "Foton finns för fotokrävande kontroller",
ok: fotoKravUtanFoto === 0,
kravs: true,
detalj: fotoKravUtanFoto > 0 ? `${fotoKravUtanFoto} fotokrävande kontroll(er) utan bild i loggen.` : undefined,
});
const hypoteser = handelser.filter((h) => h.typ === "hypotes").length;
rader.push({
id: "hypoteser",
rubrik: "Hypoteser redovisas som ej verifierade",
ok: true,
kravs: false,
detalj: hypoteser > 0 ? `${hypoteser} hypotes(er) markeras 🔴 i rapporten — aldrig som konstaterade fel.` : undefined,
});
rader.push({
id: "evidensniva",
rubrik: `Evidensnivå: ${EVIDENS_LABEL[evidensNiva(arende)]}`,
ok: evidensNiva(arende) !== "E0",
kravs: true,
detalj: evidensNiva(arende) === "E0" ? "Ingen evidens i loggen ännu." : undefined,
});
return rader;
}
// Sant när alla obligatoriska grindrader är gröna — först då kan
// slutrapporten genereras.
export function grindGodkand(arende: Arende, metodik: Metodik): boolean {
return kvalitetsgrind(arende, metodik).every((rad) => rad.ok || !rad.kravs);
}
+5 -2
View File
@@ -40,6 +40,8 @@ export function arAvslutat(arende: Arende): boolean {
export interface UtfordKontroll {
text: string;
resultat?: string;
// Satt när underlaget inte kunde tas fram (dokumenterad orsak).
undantag?: string;
tidpunkt: string;
anvandare: string;
}
@@ -49,7 +51,7 @@ export function utfordaKontroller(arende: Arende): UtfordKontroll[] {
for (const post of arende.handelser) {
const h = post.handelse;
if (h.typ === "kontroll_utford") {
resultat.push({ text: h.text, resultat: h.resultat, tidpunkt: post.tidpunkt, anvandare: post.anvandare });
resultat.push({ text: h.text, resultat: h.resultat, undantag: h.undantag, tidpunkt: post.tidpunkt, anvandare: post.anvandare });
}
}
return resultat;
@@ -226,7 +228,8 @@ export function overlamningstext(arende: Arende, metodik: Metodik, nu?: string):
rader.push("");
rader.push("Utförda kontroller:");
if (b.utfordaKontroller.length === 0) rader.push(" (inga ännu)");
for (const k of b.utfordaKontroller) rader.push(`${k.text}${k.resultat ? `${k.resultat}` : ""}`);
for (const k of b.utfordaKontroller)
rader.push(k.undantag ? `${k.text} — underlag saknas: ${k.undantag}` : `${k.text}${k.resultat ? `${k.resultat}` : ""}`);
rader.push("");
rader.push("Observationer:");
if (b.observationer.length === 0) rader.push(" (inga ännu)");
+211 -22
View File
@@ -26,7 +26,16 @@ import {
type Delning,
type DelningsNiva,
} from "@/felsokning/plattform";
import { AI_RADTYP_LABEL, fragaAi, granskaUnderlag, sammanfattaOverlamning } from "@/felsokning/ai";
import {
AI_RADTYP_LABEL,
fragaAi,
granskaUnderlag,
lasAvInstrument,
sammanfattaOverlamning,
type InstrumentTolkning,
} from "@/felsokning/ai";
import { byggDemoInstrument } from "@/felsokning/demo";
import { ECM_VERSION, UNDANTAGSORSAKER, grindGodkand, kvalitetsgrind } from "@/felsokning/ecm";
import { FelsokningSkal, NivaBadge, Panel, StorKnapp, TextFalt } from "@/felsokning/ui";
import { skalaNerFoto, tidKlockslag } from "@/felsokning/format";
@@ -273,7 +282,7 @@ function KontextPanel({
<span className="font-semibold text-[#00437A]">Nästa steg:</span> {senasteAi.handelse.nastaSteg}
</p>
<p className="mt-1 text-[10px] uppercase tracking-wide text-[#707070]">
AI-underlag · presenteras aldrig som konstaterat fel
Beslutsstöd · presenteras aldrig som konstaterat fel
</p>
</Panel>
)}
@@ -387,13 +396,13 @@ function GuideFlik({
<Panel
rubrik={
senasteAiSvar?.handelse.typ === "ai_svar"
? `AI-handledning (${senasteAiSvar.handelse.modell})`
: "AI-handledning"
? `Handledning (${senasteAiSvar.handelse.modell})`
: "Handledning"
}
>
{aiStatus === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">AI analyserar </p>}
{aiStatus === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Systemet analyserar </p>}
{aiStatus === "fel" && (
<p className="mb-2 font-semibold text-[#8B1A1A]">AI-svaret kunde inte hämtas försök igen. Metodiken fortsätter som vanligt.</p>
<p className="mb-2 font-semibold text-[#8B1A1A]">Analysen kunde inte hämtas försök igen. Metodiken fortsätter som vanligt.</p>
)}
{senasteAiSvar && senasteAiSvar.handelse.typ === "ai_svar" && (
<>
@@ -560,14 +569,74 @@ function KontrollKort({ steg, skicka }: { steg: NastaSteg; skicka: (h: Handelse)
</StorKnapp>
</>
)}
<Undantag
vidUndantag={(orsak) =>
skicka({
typ: "kontroll_utford",
stegId: steg.steg.id,
kontrollId: kontroll.id,
text: kontroll.text,
undantag: orsak,
})
}
/>
</>
);
}
// ECM-regeln: en kontroll kan bara slutföras med evidens ELLER ett
// uttryckligt undantag — "underlaget kan inte tas fram" — där orsaken
// alltid är obligatorisk och loggas i händelseloggen.
function Undantag({ vidUndantag }: { vidUndantag: (orsak: string) => void }) {
const [oppen, setOppen] = useState(false);
const [orsak, setOrsak] = useState("");
if (!oppen) {
return (
<button
onClick={() => setOppen(true)}
className="mt-2 w-full py-1 text-[12px] font-medium text-[#707070] underline-offset-2 hover:text-[#8B1A1A] hover:underline print:hidden"
>
Underlag kan inte tas fram
</button>
);
}
return (
<div className="mt-3 rounded border border-[#E0C36A] bg-[#FFF8E1] p-2">
<p className="mb-2 text-[12px] font-semibold text-[#9A6700]">
Kontrollen dokumenteras utan underlag ange orsak (obligatoriskt). Detta flaggas i brief och rapport.
</p>
<div className="mb-2 grid grid-cols-1 gap-1">
{UNDANTAGSORSAKER.map((val) => (
<button
key={val}
onClick={() => setOrsak(val)}
className={`min-h-8 rounded border px-2 text-left text-[12px] font-medium ${
orsak === val ? "border-[#00437A] bg-[#D6E4F2]" : "border-[#C6C6C6] bg-white"
}`}
>
{val}
</button>
))}
</div>
<TextFalt label="Eller egen orsak" varde={orsak && !UNDANTAGSORSAKER.includes(orsak) ? orsak : ""} satt={setOrsak} rost />
<div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setOppen(false)}>
Avbryt
</StorKnapp>
<StorKnapp variant="fara" disabled={!orsak.trim()} onClick={() => vidUndantag(orsak.trim())}>
Dokumentera undantag
</StorKnapp>
</div>
</div>
);
}
const DOKTYPER = [
{ id: "observation", label: "Observation" },
{ id: "matvarde", label: "Mätvärde" },
{ id: "foto", label: "Foto" },
{ id: "instrument", label: "📷 Instrument" },
{ id: "hypotes", label: "Hypotes" },
{ id: "kommentar", label: "Kommentar" },
] as const;
@@ -587,6 +656,9 @@ function SnabbDokumentation({
const [text, setText] = useState("");
const [varde, setVarde] = useState("");
const filRef = useRef<HTMLInputElement>(null);
const instRef = useRef<HTMLInputElement>(null);
const [avlasning, setAvlasning] = useState<{ foto: string; tolkning: InstrumentTolkning; demo?: boolean } | null>(null);
const [laserAv, setLaserAv] = useState(false);
const aterstall = () => {
setTyp(null);
@@ -594,6 +666,44 @@ function SnabbDokumentation({
setVarde("");
};
// Visual-first: instrumentet/skärmen fotograferas, bildtolkningen
// extraherar värdena och teknikern bekräftar innan något loggas.
// Originalbilden bevaras alltid som evidens bredvid de strukturerade
// värdena. I lokalt läge visas en tydligt märkt demo-avläsning.
const lasAv = async (fil: File) => {
setLaserAv(true);
const foto = await skalaNerFoto(fil);
try {
const resultat = await lasAvInstrument(foto);
if (resultat && resultat.tolkning.varden.length > 0) {
setAvlasning({ foto, tolkning: resultat.tolkning });
} else if (!resultat) {
setAvlasning({ foto, tolkning: byggDemoInstrument(), demo: true });
} else {
skicka({ typ: "foto", beskrivning: "Instrumentfoto — inga värden kunde läsas", dataUrl: foto });
}
} catch {
skicka({ typ: "foto", beskrivning: "Instrumentfoto — avläsningen kunde inte nås", dataUrl: foto });
}
setLaserAv(false);
};
const sparaAvlasning = () => {
if (!avlasning) return;
// Originalet först, sedan varje bekräftat värde som eget mätvärde —
// strukturerad data ersätter aldrig originalevidensen.
skicka({ typ: "foto", beskrivning: `Instrumentavläsning (${avlasning.tolkning.instrumenttyp})`, dataUrl: avlasning.foto });
for (const v of avlasning.tolkning.varden) {
skicka({ typ: "matvarde", beskrivning: v.beskrivning, varde: v.varde, enhet: v.enhet });
}
paSparad?.(
`Instrumentavläsning (${avlasning.tolkning.instrumenttyp}): ${avlasning.tolkning.varden
.map((v) => `${v.beskrivning} = ${v.varde}${v.enhet ? ` ${v.enhet}` : ""}`)
.join("; ")}`,
);
setAvlasning(null);
};
const spara = () => {
const t = text.trim();
if (typ === "observation" && t) {
@@ -621,7 +731,13 @@ function SnabbDokumentation({
{DOKTYPER.map((d) => (
<button
key={d.id}
onClick={() => (d.id === "foto" ? filRef.current?.click() : setTyp(typ === d.id ? null : d.id))}
onClick={() =>
d.id === "foto"
? filRef.current?.click()
: d.id === "instrument"
? instRef.current?.click()
: setTyp(typ === d.id ? null : d.id)
}
className={`min-h-9 rounded border text-[12px] font-semibold transition-colors ${
typ === d.id
? "border-[#00437A] bg-[#00437A] text-white"
@@ -646,7 +762,48 @@ function SnabbDokumentation({
e.target.value = "";
}}
/>
{typ && typ !== "foto" && (
<input
ref={instRef}
type="file"
accept="image/*"
capture="environment"
className="hidden"
onChange={(e) => {
const fil = e.target.files?.[0];
if (fil) lasAv(fil);
e.target.value = "";
}}
/>
{laserAv && <p className="mt-2 animate-pulse text-center text-[12px] font-semibold text-[#00437A]">Systemet läser av instrumentet </p>}
{avlasning && (
<div className="mt-3 rounded border border-[#C6C6C6] bg-white p-2">
{avlasning.demo && (
<p className="mb-2 rounded border border-[#E0C36A] bg-[#FFF8E1] p-1.5 text-[11px] font-semibold text-[#9A6700]">
Demo-avläsning bildtolkningen kräver inloggning. Värdena är exempeldata.
</p>
)}
<p className="mb-1 text-[11px] font-semibold uppercase tracking-wide text-[#4A5560]">
Avläst: {avlasning.tolkning.instrumenttyp} bekräfta innan något loggas
</p>
{avlasning.tolkning.varden.map((v, i) => (
<div key={i} className="flex items-center justify-between gap-2 border-b border-[#EBEBEB] py-1 text-[13px] last:border-0">
<span>
{v.beskrivning}: <span className="font-semibold">{v.varde}{v.enhet ? ` ${v.enhet}` : ""}</span>
</span>
<span className={`text-[11px] font-semibold ${v.konfidens >= 0.95 ? "text-[#1E6B34]" : v.konfidens >= 0.8 ? "text-[#9A6700]" : "text-[#8B1A1A]"}`}>
{v.konfidens >= 0.95 ? "✓" : `${Math.round(v.konfidens * 100)} %`}
</span>
</div>
))}
<div className="mt-2 grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => setAvlasning(null)}>
Förkasta
</StorKnapp>
<StorKnapp onClick={sparaAvlasning}>Spara foto + värden</StorKnapp>
</div>
</div>
)}
{typ && typ !== "foto" && typ !== "instrument" && (
<div className="mt-3">
{typ === "hypotes" && (
<p className="mb-2 text-[12px] font-semibold text-[#8B1A1A]">
@@ -716,10 +873,10 @@ function OverlamningDialog({
</pre>
{aiLage === "vilar" && (
<StorKnapp variant="sekundar" className="mb-3" onClick={komplettera}>
AI-komplettera: risker &amp; osäkerheter
Komplettera analysen: risker &amp; osäkerheter
</StorKnapp>
)}
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">AI sammanfattar </p>}
{aiLage === "arbetar" && <p className="mb-3 animate-pulse font-semibold text-[#00437A]">Systemet sammanfattar </p>}
{aiLage === "fel" && (
<p className="mb-3 font-semibold text-[#8B1A1A]">Kunde inte hämtas kräver inloggning. Överlämningen fungerar ändå.</p>
)}
@@ -901,12 +1058,12 @@ function BriefFlik({
return (
<>
{!b.avslutat && (
<Panel rubrik="AI-granskning">
<Panel rubrik="Granskning av underlaget">
<p className="mb-2 text-[#333333]">
Låt AI:n igenom hela underlaget innan du lämnar det vidare: motsägelser mellan
Låt systemet igenom hela underlaget innan du lämnar det vidare: motsägelser mellan
observationer, luckor i dokumentationen och slutsatser som saknar stöd.
</p>
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">AI granskar underlaget </p>}
{granskning === "arbetar" && <p className="mb-2 animate-pulse font-semibold text-[#00437A]">Systemet granskar underlaget </p>}
{granskning === "fel" && (
<p className="mb-2 font-semibold text-[#8B1A1A]">Granskningen kunde inte hämtas kräver inloggning. Försök igen.</p>
)}
@@ -920,11 +1077,11 @@ function BriefFlik({
<p className="mt-1 text-[14px]">
<span className="font-semibold text-[#00437A]">Nästa steg:</span> {senasteAi.handelse.nastaSteg}
</p>
<p className="mt-1 text-[11px] text-[#707070]">Senaste AI-svar ({senasteAi.handelse.modell})</p>
<p className="mt-1 text-[11px] text-[#707070]">Senaste analys ({senasteAi.handelse.modell})</p>
</div>
)}
<StorKnapp variant="sekundar" disabled={granskning === "arbetar"} onClick={granska}>
🔍 AI-granska underlaget
🔍 Granska underlaget
</StorKnapp>
</Panel>
)}
@@ -945,12 +1102,18 @@ function BriefFlik({
</Panel>
<Panel rubrik="Utförda kontroller">
{b.utfordaKontroller.length === 0 && <p className="text-[#4A5560]">Inga ännu.</p>}
{b.utfordaKontroller.map((k, i) => (
<p key={i} className="py-0.5 text-[14px]">
{k.text}
{k.resultat && <span className="text-[#4A5560]"> {k.resultat}</span>}
</p>
))}
{b.utfordaKontroller.map((k, i) =>
k.undantag ? (
<p key={i} className="py-0.5 text-[14px] text-[#9A6700]">
{k.text} underlag saknas: {k.undantag}
</p>
) : (
<p key={i} className="py-0.5 text-[14px]">
{k.text}
{k.resultat && <span className="text-[#4A5560]"> {k.resultat}</span>}
</p>
),
)}
</Panel>
<Panel rubrik="Observationer">
{b.observationer.length === 0 && <p className="text-[#4A5560]">Inga ännu.</p>}
@@ -1035,15 +1198,41 @@ function RapportFlik({
skicka({ typ: "export_skapad", format: "JSON", version });
};
const grind = kvalitetsgrind(arende, metodik);
const godkand = grindGodkand(arende, metodik);
return (
<>
{/* ECM-kvalitetsgrinden: slutrapporten kan inte genereras förrän
varje obligatoriskt påstående har evidens eller dokumenterat
undantag i händelseloggen. */}
<Panel rubrik={`Kvalitetsgrind — ECM v${ECM_VERSION}`}>
{grind.map((rad) => (
<div key={rad.id} className="border-b border-[#EBEBEB] py-1 last:border-0">
<p className="text-[13px]">
<span className={rad.ok ? "text-[#1E6B34]" : rad.kravs ? "text-[#8B1A1A]" : "text-[#9A6700]"}>
{rad.ok ? "✅" : rad.kravs ? "❌" : "⚠️"}
</span>{" "}
{rad.rubrik}
{!rad.kravs && <span className="text-[11px] text-[#707070]"> (rekommenderas)</span>}
</p>
{rad.detalj && <p className="pl-6 text-[12px] text-[#707070]">{rad.detalj}</p>}
</div>
))}
{!godkand && (
<p className="mt-2 text-[12px] font-semibold text-[#8B1A1A]">
Rapporten kan inte genereras förrän varje har evidens eller ett dokumenterat undantag
(Underlag kan inte tas fram i guiden).
</p>
)}
</Panel>
<Panel rubrik="Kundrapport">
<p className="mb-2 text-[#333333]">
Delningsbar sammanställning av utfört arbete. Granska innehållet innan rapporten delas bilder kan
innehålla uppgifter om andra kunder.
</p>
<div className="grid grid-cols-2 gap-2">
<StorKnapp variant="sekundar" onClick={() => window.print()}>
<StorKnapp variant="sekundar" disabled={!godkand} onClick={() => window.print()}>
Skriv ut / PDF
</StorKnapp>
<StorKnapp variant="sekundar" onClick={exporteraJson}>
+4 -4
View File
@@ -45,7 +45,7 @@ function PlattformInloggning() {
<div className="flex items-center justify-between gap-3">
<p className="text-[14px]">
<span className="font-semibold">{konto.namn}</span> · {ROLL_LABEL[konto.roll]} ·{" "}
{konto.organisation} synk och AI aktiva.
{konto.organisation} synk och beslutsstöd aktiva.
</p>
<button
className="whitespace-nowrap rounded border border-[#ADADAD] px-4 py-2 font-semibold text-[#333333] hover:border-[#8FA8C0]"
@@ -78,7 +78,7 @@ function PlattformInloggning() {
{lage === "stangd" ? (
<>
<p className="mb-2 text-[#333333]">
Logga in för synk mellan enheter, samarbete i ärenden och AI-stöd. Utan inloggning
Logga in för synk mellan enheter, samarbete i ärenden och beslutsstöd. Utan inloggning
arbetar appen i lokalt läge. Ny organisation? Skapa konto du blir systemadministratör.
</p>
<div className="grid grid-cols-2 gap-2">
@@ -317,9 +317,9 @@ export default function Arendelista() {
</Link>
)}
<Panel rubrik="AI-stöd">
<Panel rubrik="Beslutsstöd">
<p className="text-[#333333]">
När du är inloggad hjälper AI:n dig medan du arbetar: föreslår nästa steg utifrån det du
När du är inloggad hjälper systemet dig medan du arbetar: föreslår nästa steg utifrån det du
dokumenterar, granskar hela ärendet begäran och skriver utkast till överlämningen. Den
skiljer alltid verifierat och hypotes och slår aldrig fast en felorsak som inte är
bekräftad. Inget att installera eller ställa in; allt ingår i tjänsten. Utan inloggning
+44
View File
@@ -31,6 +31,7 @@ Absoluta regler:
- "hypotes": möjlig felorsak som KRÄVER verifiering — formulera alltid vad som skulle verifiera den.
- "rekommendation": nästa verifierbara kontroll eller mätning.
- Om underlaget är otillräckligt: säg det uttryckligen i en observation och rekommendera vad som behöver dokumenteras.
- Skriv aldrig "OK", "kontrollerad", "inga fel" eller "åtgärdad" om evidens saknas i underlaget — skriv i stället "Evidens saknas" och begär rätt underlag: foto för det synliga, video med ljud för det som låter, video för det som rör sig, mätvärde för det som mäts, foto av skärmen när ett instrument eller en diagnosdator visar informationen.
- Svara på svenska, konsekvent, kortfattat och metodiskt. "nastaSteg" är EN konkret, verifierbar åtgärd.`;
const SVARS_SCHEMA = {
@@ -115,6 +116,40 @@ Regler:
- Normalisera: registreringsnummer i versaler utan mellanslag, VIN med 17 tecken, datum som ÅÅÅÅ-MM-DD, mätarställning med enhet.
- "felbeskrivning" är kundens beskrivna problem/arbetsbegäran om en sådan finns i dokumentet.`;
// Visual-first: instrument och diagnosskärmar fotograferas i stället för
// att integreras — kameran är det universella gränssnittet.
const INSTRUMENT_SCHEMA = {
type: "object",
properties: {
instrumenttyp: { type: "string" },
varden: {
type: "array",
items: {
type: "object",
properties: {
beskrivning: { type: "string" },
varde: { type: "string" },
enhet: { type: "string" },
konfidens: { type: "number", minimum: 0, maximum: 1 },
},
required: ["beskrivning", "varde", "konfidens"],
additionalProperties: false,
},
},
},
required: ["instrumenttyp", "varden"],
additionalProperties: false,
};
const INSTRUMENT_REGLER = `Du läser av ett foto av ett instrument eller en skärm i en fordonsverkstad: multimeter, diagnosdator, oscilloskop, batteritestare, mätarkluster, manometer, utskrift eller etikett.
Regler:
- Identifiera instrumenttypen och extrahera ENDAST värden som faktiskt går att läsa i bilden. Hitta aldrig på värden.
- Varje värde får en beskrivning (t.ex. "Batterispänning", "Felkod P0301 misständning cylinder 1"), värdet som text och enhet när den syns (V, A, Ω, bar, °C, rpm …).
- Felkoder returneras som egna värden med kod och klartext om skärmen visar den.
- "konfidens" är din läs-säkerhet 01; sänk vid oskarp bild, reflexer eller delvis skymda siffror.`;
const MAX_BILD_LANGD = 3_000_000;
interface UppgiftKonfig {
@@ -175,6 +210,15 @@ Gissa inte: välj "generisk" om beskrivningen inte tydligt hör till en specifik
},
// Ärendestart: teknikern fotograferar arbetsordern; vi läser dokumentet
// och returnerar strukturerade fält med konfidens per värde.
// Visual-first: foto av instrument/diagnosskärm → strukturerade värden.
instrumentavlasning: {
modell: "claude-sonnet-5",
effort: "low",
maxTokens: 1024,
system: INSTRUMENT_REGLER,
schema: INSTRUMENT_SCHEMA,
bild: true,
},
dokumenttolkning: {
modell: "claude-sonnet-5",
effort: "low",