From 2202737d48153f96c9a40e4f7ff092389b13eb83 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 00:55:23 +0000 Subject: [PATCH] =?UTF-8?q?K=C3=B6r=20Claude=20som=20AI-modell=20i=20Guida?= =?UTF-8?q?d=20Fels=C3=B6kning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AI-motor (ai.ts): Claude Opus 5 via officiella TypeScript-SDK:n. Teknikerns bekräftade dokumentation skickas med ärendebriefen som kontext; svaret är schema-bundet (structured outputs) och strikt klassificerat enligt AI-reglerna: observation, verifierat, hypotes (kräver alltid verifiering) och rekommendation, plus ETT konkret nästa steg. Fryst systemprompt med promptcache; automatisk server-side fallback till Anthropics rekommenderade reservmodell när säkerhetsklassificerarna avböjer; refusal hanteras innan innehållet läses. - Onboarding enligt direktivet: organisationens Claude API-nyckel läggs in på startsidan (maskerat fält). Utan nyckel guidar den deterministiska metodiken ensam. MVP-varning: nyckeln lagras på enheten och anropet görs från webbläsaren — i produktion via backend. - AI-svaren loggas som händelser med modellnamn och visas i guiden; de är arbetsmaterial och filtreras ur kundrapport, Live Share och databasens delningsfunktion, precis som hypoteser. - Master Prompt uppdaterad: AI-wrapper ovanpå Claude API, standard- modell claude-opus-5. Verifierat: 25 vitest-tester gröna (inkl. promptbygge, svarstolkning och systemprompt-regler), produktionsbygge ok, Playwright-röktest genom hela flödet. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt --- docs/MASTER-PROMPT.md | 4 +- docs/MVP.md | 2 +- package-lock.json | 63 ++++++++ package.json | 1 + src/felsokning/DelatArendeVy.tsx | 2 +- src/felsokning/__tests__/ai.test.ts | 48 ++++++ src/felsokning/ai.ts | 140 ++++++++++++++++++ src/felsokning/domain.ts | 10 ++ src/felsokning/store.ts | 6 + src/felsokning/ui.tsx | 10 +- src/pages/felsokning/ArendeSida.tsx | 80 ++++++++-- src/pages/felsokning/Arendelista.tsx | 41 ++++- .../20260802230000_guidad_felsokning.sql | 2 +- 13 files changed, 393 insertions(+), 16 deletions(-) create mode 100644 src/felsokning/__tests__/ai.test.ts create mode 100644 src/felsokning/ai.ts diff --git a/docs/MASTER-PROMPT.md b/docs/MASTER-PROMPT.md index d9ccc6e..377c476 100644 --- a/docs/MASTER-PROMPT.md +++ b/docs/MASTER-PROMPT.md @@ -10,7 +10,7 @@ Det här är ett produktdirektiv, inte en teknisk specifikation. Visionen och re Bygg en produktionsredo SaaS-plattform med namnet **Guidad Felsökning**. -Plattformen är en AI-wrapper ovanpå en LLM-leverantörs API och fungerar som ett professionellt arbetsverktyg för mekaniker och servicetekniker. Den ska inte ersätta teknisk kompetens eller tillverkarens dokumentation, utan vägleda användaren genom en strukturerad felsökningsprocess, dokumentera allt arbete och skapa full spårbarhet. +Plattformen är en AI-wrapper ovanpå Claude API (Anthropic) och fungerar som ett professionellt arbetsverktyg för mekaniker och servicetekniker. Standardmodell: Claude Opus 5 (`claude-opus-5`), med Anthropics rekommenderade reservmodell som automatisk fallback när en förfrågan avböjs av säkerhetsklassificerarna. Den ska inte ersätta teknisk kompetens eller tillverkarens dokumentation, utan vägleda användaren genom en strukturerad felsökningsprocess, dokumentera allt arbete och skapa full spårbarhet. Målet är att lansera en stabil, enkel och köpvärdig beta-version. @@ -53,7 +53,7 @@ Första gången en kund loggar in: 1. Skapa företag 2. Lägg till logotyp 3. Lägg till användare -4. Lägg till API-nyckel för AI-tjänsten +4. Lägg till Claude API-nyckel 5. Lägg till eventuella integrationer Klart. Hela onboarding ska ta mindre än fem minuter. diff --git a/docs/MVP.md b/docs/MVP.md index 0937fe5..3666ebb 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -17,7 +17,7 @@ Demomanus för visning: [DEMO.md](DEMO.md). Knappen **Skapa demoärende** på st | Direktivets kärna | Status i MVP | | --- | --- | | Objektidentifiering först | ✅ Manuell inmatning (reg.nr/VIN/serienummer/maskinnummer) med bekräftelsesteg. QR/streckkod/OCR är markerade som kommande. | -| AI-guidad felsökning | ✅ Deterministisk metodikmotor: en fråga i taget, stora knappar. Två metodiker (vibration + generisk) som väljs utifrån felbeskrivningen. LLM-integrationen ansluter i samma motor senare. | +| AI-guidad felsökning | ✅ Deterministisk metodikmotor (en fråga i taget, tre metodiker) **plus Claude-handledning**: med organisationens Claude API-nyckel (läggs in på startsidan) svarar Claude Opus 5 skriftligt på varje bekräftad dokumentation — klassificerat som observation/verifierat/hypotes/rekommendation enligt AI-reglerna, schema-bundet så svaret aldrig kan läsas som fritext, med automatisk fallback till Anthropics rekommenderade reservmodell vid avböjd förfrågan. Svaren loggas som händelser (interna — delas aldrig i kundvyer). Utan nyckel guidar metodiken ensam. MVP: nyckeln lagras på enheten och anropet görs från webbläsaren; i produktion går det via backend. | | Arbetslogg | ✅ Append-only händelselogg med tidsstämpel och användare på varje post. Ingenting skrivs över. | | Tidredovisning | ✅ Kategorier (aktiv felsökning, väntetid, provkörning …) via kategoribyten i loggen; paus räknas inte i total tid. Inaktivitetsfråga efter 20 min utan händelser. | | Dokumentation | ✅ Observationer, mätvärden, foton (nedskalade), kommentarer och hypoteser. Hypoteser märks alltid 🔴 och kan aldrig loggas som konstaterade fel. | diff --git a/package-lock.json b/package-lock.json index 04c0e94..7183894 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "vite_react_shadcn_ts", "version": "0.0.0", "dependencies": { + "@anthropic-ai/sdk": "^0.115.0", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", @@ -105,6 +106,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.115.0.tgz", + "integrity": "sha512-BJrFIVyjNuU8lfDyIJTvlRYzgQg+zEl78BxE7fq8esULsGz9IRQvGtW5spq3tydmtjQb/GFdooKGdGsetpx+lQ==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -2537,6 +2559,12 @@ "win32" ] }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, "node_modules/@stripe/react-stripe-js": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-6.2.0.tgz", @@ -4915,6 +4943,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -5568,6 +5602,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7318,6 +7365,16 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, "node_modules/std-env": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", @@ -7706,6 +7763,12 @@ "node": ">=12" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", diff --git a/package.json b/package.json index e27141f..a560c4c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "test:watch": "vitest" }, "dependencies": { + "@anthropic-ai/sdk": "^0.115.0", "@hookform/resolvers": "^3.10.0", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-alert-dialog": "^1.1.14", diff --git a/src/felsokning/DelatArendeVy.tsx b/src/felsokning/DelatArendeVy.tsx index 921a1cf..a1f1f8d 100644 --- a/src/felsokning/DelatArendeVy.tsx +++ b/src/felsokning/DelatArendeVy.tsx @@ -18,7 +18,7 @@ export function DelatArendeVy({ arende, nu, notis }: { arende: Arende; nu: strin const bilder = foton(arende); const matvarden = arende.handelser.filter((p) => p.handelse.typ === "matvarde"); const kundposter = arende.handelser.filter( - (p) => p.handelse.typ !== "kategori_byte" && p.handelse.typ !== "hypotes", + (p) => !["kategori_byte", "hypotes", "ai_svar"].includes(p.handelse.typ), ); const pagaende = b.rekommenderatNastaSteg[0]; diff --git a/src/felsokning/__tests__/ai.test.ts b/src/felsokning/__tests__/ai.test.ts new file mode 100644 index 0000000..f0fe551 --- /dev/null +++ b/src/felsokning/__tests__/ai.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { AI_MODELL, SYSTEM_PROMPT, byggAnvandarPrompt, tolkaAiSvar } from "../ai"; +import { VIBRATION_METODIK } from "../metodik"; +import { brief } from "../projektioner"; +import { byggDemoArende } from "../demo"; + +describe("AI-motorn", () => { + it("använder Claude Opus 5", () => { + expect(AI_MODELL).toBe("claude-opus-5"); + }); + + it("systemprompten kodar AI-reglerna", () => { + expect(SYSTEM_PROMPT).toContain("Hitta aldrig på fakta"); + expect(SYSTEM_PROMPT).toContain("aldrig en hypotes som ett konstaterat fel"); + expect(SYSTEM_PROMPT).toContain("KRÄVER verifiering"); + expect(SYSTEM_PROMPT).toContain("kortfattat"); + }); + + it("användarprompten byggs ur ärendebriefen och den nya inmatningen", () => { + const arende = byggDemoArende(1); + const prompt = byggAnvandarPrompt( + brief(arende, VIBRATION_METODIK), + VIBRATION_METODIK.namn, + "Reläet klickar inte.", + ); + expect(prompt).toContain("Volvo XC60 D4 2019"); + expect(prompt).toContain("Felbeskrivning:"); + expect(prompt).toContain("Kontrollera lufttryck"); + expect(prompt).toContain("Ej kontrollerat enligt metodiken:"); + expect(prompt).toContain("Teknikerns nya inmatning: Reläet klickar inte."); + }); + + it("tolkar giltiga svar och kastar på ogiltiga", () => { + const svar = tolkaAiSvar({ + rader: [ + { typ: "verifierat", text: "Matningsspänning finns på stift 14." }, + { typ: "okand_typ", text: "ska filtreras bort" }, + ], + nastaSteg: "Kontrollera jordanslutningen på stift 7.", + }); + expect(svar.rader).toHaveLength(1); + expect(svar.rader[0].typ).toBe("verifierat"); + expect(svar.nastaSteg).toContain("stift 7"); + + expect(() => tolkaAiSvar({ rader: "inte en lista", nastaSteg: "x" })).toThrow(); + expect(() => tolkaAiSvar(null)).toThrow(); + }); +}); diff --git a/src/felsokning/ai.ts b/src/felsokning/ai.ts new file mode 100644 index 0000000..c3de370 --- /dev/null +++ b/src/felsokning/ai.ts @@ -0,0 +1,140 @@ +// AI-motor: Claude som diagnostikhandledare. +// +// Kommunikationsmodellen är text in, text ut: teknikerns (bekräftade) +// inmatning skickas tillsammans med ärendebriefen, och Claude svarar +// kortfattat och klassificerat enligt AI-reglerna i Master Prompt — +// observation / verifierat / hypotes / rekommendation. Svaret är +// schema-bundet (structured outputs), så det kan aldrig komma tillbaka +// som fritext som råkar läsas som ett konstaterat fel. +// +// MVP: tenantens egen API-nyckel används direkt från klienten +// (dangerouslyAllowBrowser). I produktionsversionen flyttas anropet +// bakom plattformens backend så att nycklar aldrig lämnar servern. + +import Anthropic from "@anthropic-ai/sdk"; +import type { Brief } from "./projektioner"; + +export const AI_MODELL = "claude-opus-5"; + +export type AiRadTyp = "observation" | "verifierat" | "hypotes" | "rekommendation"; + +export const AI_RADTYP_LABEL: Record = { + observation: "Observation", + verifierat: "Verifierat", + hypotes: "🔴 Hypotes", + rekommendation: "Rekommendation", +}; + +export interface AiRad { + typ: AiRadTyp; + text: string; +} + +export interface AiSvar { + rader: AiRad[]; + nastaSteg: string; +} + +const SVARS_SCHEMA = { + type: "object", + properties: { + rader: { + type: "array", + items: { + type: "object", + properties: { + typ: { type: "string", enum: ["observation", "verifierat", "hypotes", "rekommendation"] }, + text: { type: "string" }, + }, + required: ["typ", "text"], + additionalProperties: false, + }, + }, + nastaSteg: { type: "string" }, + }, + required: ["rader", "nastaSteg"], + additionalProperties: false, +} as const; + +// Fryst systemprompt (cachas mellan anrop) — all ärendespecifik kontext +// ligger i användarmeddelandet. +export const SYSTEM_PROMPT = `Du är en digital felsökningshandledare för professionella tekniker i plattformen Guidad Felsökning. Du är inte en AI-mekaniker: du ersätter aldrig teknikerns kompetens eller tillverkarens dokumentation — du säkerställer att arbetet sker metodiskt och dokumenteras korrekt. + +Absoluta regler: +- Hitta aldrig på fakta. Låtsas aldrig veta. Gissa aldrig. +- Presentera aldrig en hypotes som ett konstaterat fel. +- Skilj strikt mellan raderna du returnerar: + - "verifierat": endast det som är belagt av mätvärden eller dokumenterade kontroller i underlaget. + - "observation": det som konstaterats utan slutsats. + - "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. + +Arbetssätt: en kontroll i taget, inte långa utläggningar. Svara på svenska, konsekvent, kortfattat och metodiskt — max fyra rader plus nästa steg. "nastaSteg" är EN konkret, verifierbar åtgärd.`; + +export function byggAnvandarPrompt(brief: Brief, metodikNamn: string, inmatning: string): string { + const rader: string[] = []; + rader.push(`Metodik: ${metodikNamn}`); + if (brief.objekt) rader.push(`Objekt: ${brief.objekt.beskrivning} (${brief.objekt.identifierare})`); + if (brief.felbeskrivning) rader.push(`Felbeskrivning: ${brief.felbeskrivning}`); + if (brief.utfordaKontroller.length > 0) { + rader.push("Utförda kontroller:"); + for (const k of brief.utfordaKontroller) rader.push(`- ${k.text}${k.resultat ? `: ${k.resultat}` : ""}`); + } + if (brief.observationer.length > 0) { + rader.push("Observationer och mätvärden:"); + for (const o of brief.observationer) rader.push(`- ${o}`); + } + if (brief.ejKontrollerat.length > 0) { + rader.push(`Ej kontrollerat enligt metodiken: ${brief.ejKontrollerat.join("; ")}`); + } + rader.push(""); + rader.push(`Teknikerns nya inmatning: ${inmatning}`); + return rader.join("\n"); +} + +export function tolkaAiSvar(data: unknown): AiSvar { + const svar = data as AiSvar; + if (!svar || !Array.isArray(svar.rader) || typeof svar.nastaSteg !== "string") { + throw new Error("Oväntat AI-svarsformat"); + } + const giltiga: AiRadTyp[] = ["observation", "verifierat", "hypotes", "rekommendation"]; + const rader = svar.rader.filter( + (rad): rad is AiRad => !!rad && giltiga.includes(rad.typ) && typeof rad.text === "string", + ); + return { rader, nastaSteg: svar.nastaSteg }; +} + +export async function fragaAi( + apiNyckel: string, + brief: Brief, + metodikNamn: string, + inmatning: string, +): Promise { + const klient = new Anthropic({ apiKey: apiNyckel, dangerouslyAllowBrowser: true }); + const svar = await klient.beta.messages.create({ + model: AI_MODELL, + max_tokens: 1024, + // Verkstadsgolvet är latenskänsligt och uppgiften är väl avgränsad + // av briefen — medium balanserar svarstid mot kvalitet. + output_config: { + effort: "medium", + format: { type: "json_schema", schema: SVARS_SCHEMA }, + }, + // Avböjer säkerhetsklassificerarna faller anropet automatiskt + // tillbaka till Anthropics rekommenderade reservmodell. + betas: ["server-side-fallback-2026-07-01"], + fallbacks: "default", + system: [{ type: "text", text: SYSTEM_PROMPT, cache_control: { type: "ephemeral" } }], + messages: [{ role: "user", content: byggAnvandarPrompt(brief, metodikNamn, inmatning) }], + } as Parameters[0]); + + if (svar.stop_reason === "refusal") { + throw new Error("AI-tjänsten avböjde förfrågan."); + } + const textBlock = svar.content.find((block) => block.type === "text"); + if (!textBlock || textBlock.type !== "text") { + throw new Error("AI-svaret saknade innehåll."); + } + return tolkaAiSvar(JSON.parse(textBlock.text)); +} diff --git a/src/felsokning/domain.ts b/src/felsokning/domain.ts index 9173b03..b0e77ab 100644 --- a/src/felsokning/domain.ts +++ b/src/felsokning/domain.ts @@ -52,6 +52,12 @@ export type Handelse = | { typ: "inaktivitet_forklarad"; text: string; minuter: number } | { typ: "overlamning"; fran: string; till?: string } | { typ: "export_skapad"; format: string; version: number } + | { + typ: "ai_svar"; + rader: { typ: "observation" | "verifierat" | "hypotes" | "rekommendation"; text: string }[]; + nastaSteg: string; + modell: string; + } | { typ: "arende_avslutat" }; export interface LoggPost { @@ -112,6 +118,10 @@ export function handelseRubrik(post: LoggPost): string { return h.till ? `Arbete överlämnat från ${h.fran} till ${h.till}` : `Arbete överlämnat av ${h.fran}`; case "export_skapad": return `Export skapad: ${h.format}, version ${h.version}`; + case "ai_svar": { + const forsta = h.rader[0]; + return `AI: ${forsta ? `${forsta.text} ` : ""}— Nästa steg: ${h.nastaSteg}`; + } case "arende_avslutat": return "Felsökning avslutad"; } diff --git a/src/felsokning/store.ts b/src/felsokning/store.ts index 200f76c..885e752 100644 --- a/src/felsokning/store.ts +++ b/src/felsokning/store.ts @@ -12,9 +12,13 @@ import { GENERISK_METODIK, valjMetodik, type Metodik } from "./metodik"; interface FelsokningState { anvandare: string; + // Tenantens Claude API-nyckel (MVP: lagras endast på enheten; i + // produktionsversionen hanteras nycklar av backend, aldrig i klienten). + aiNyckel: string; arenden: Record; nastaNummer: number; sattAnvandare: (namn: string) => void; + sattAiNyckel: (nyckel: string) => void; skapaArende: (objekt: Objekt, felbeskrivningText: string) => string; laggTill: (arendeId: string, handelse: Handelse) => void; // Används endast av synken: ersätter listan med den ihopflätade versionen. @@ -34,10 +38,12 @@ export const useFelsokning = create()( persist( (set, get) => ({ anvandare: "", + aiNyckel: "", arenden: {}, nastaNummer: 1, sattAnvandare: (namn) => set({ anvandare: namn.trim() }), + sattAiNyckel: (nyckel) => set({ aiNyckel: nyckel.trim() }), skapaArende: (objekt, felbeskrivningText) => { const { anvandare, nastaNummer } = get(); diff --git a/src/felsokning/ui.tsx b/src/felsokning/ui.tsx index 028b4d3..f8813ee 100644 --- a/src/felsokning/ui.tsx +++ b/src/felsokning/ui.tsx @@ -92,6 +92,7 @@ export function TextFalt({ platshallare, flerRad, rost, + losenord, }: { label: string; varde: string; @@ -99,6 +100,7 @@ export function TextFalt({ platshallare?: string; flerRad?: boolean; rost?: boolean; + losenord?: boolean; }) { // Tal-till-text skriver in i samma redigerbara fält — användaren // granskar och bekräftar alltid innan något sparas i loggen. @@ -119,7 +121,13 @@ export function TextFalt({ {flerRad ? (