Files
alva/src/felsokning/Mikrofon.tsx
T
Claude c3e831b478 Industriell ETKA-inspirerad design + Personbil/Regnr
Hela felsökningsmodulen omdesignad enligt tysk verkstadstradition
(ETKA/Bosch/Siemens-känsla): plana ljusgrå ytor, skarpa kanter, djup
marinblå primärfärg i stället för amber, tät typografi 11–15 px,
rektangulära knappar (max 4 px radie) och låg verktygsrad (~44 px)
med breadcrumb.

Ärendesidan får klassisk trekolumnslayout på skrivbord:
- Vänster: navigationsträd med ärendevyer + metodikstegens status
- Mitten: arbetsytan (guiden, en fråga i taget)
- Höger: kontextpanel med teknisk information, tillförlitlighet,
  rekommenderat nästa steg och senaste tekniska rekommendation (AI)
Smala skärmar behåller flikrad och en kolumn.

Dessutom: objekttypen "Fordon" heter nu "Personbil" och
"Registreringsnummer" har kortats till "Regnr".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EQg3rJsrQ1ZNTvkzmQAtt
2026-08-03 08:43:57 +00:00

66 lines
2.0 KiB
TypeScript

// Push-to-Talk-mikrofon: lyssnar ENDAST efter aktivt tryck, visar tydligt
// när inspelning pågår och skriver in transkriberingen i ett redigerbart
// fält. Skicka sker aldrig automatiskt.
import { useEffect, useRef, useState } from "react";
import { startaIgenkanning, stodRost } from "./rost";
export function MikrofonKnapp({ paText }: { paText: (text: string) => void }) {
const [lyssnar, setLyssnar] = useState(false);
const [interim, setInterim] = useState("");
const igenkanningRef = useRef<ReturnType<typeof startaIgenkanning>>(undefined);
const paTextRef = useRef(paText);
paTextRef.current = paText;
useEffect(() => () => igenkanningRef.current?.abort(), []);
if (!stodRost()) return null;
const stoppa = () => {
igenkanningRef.current?.stop();
igenkanningRef.current = undefined;
setLyssnar(false);
setInterim("");
};
const starta = () => {
setLyssnar(true);
igenkanningRef.current = startaIgenkanning(
({ transkript, slutgiltigt }) => {
if (slutgiltigt) {
setInterim("");
paTextRef.current(transkript.trim());
} else {
setInterim(transkript);
}
},
() => {
setLyssnar(false);
setInterim("");
},
);
};
return (
<span className="inline-flex items-center gap-2">
<button
type="button"
onClick={() => (lyssnar ? stoppa() : starta())}
aria-label={lyssnar ? "Stoppa inspelning" : "Starta röstinmatning"}
className={`min-h-8 min-w-11 rounded border px-3 text-[14px] font-semibold transition-colors ${
lyssnar
? "animate-pulse border-[#6E1414] bg-[#8B1A1A] text-white"
: "border-[#ADADAD] bg-white text-[#1A1A1A] hover:border-[#00437A]"
}`}
>
{lyssnar ? "■" : "🎤"}
</button>
{lyssnar && (
<span className="text-[12px] font-semibold text-[#8B1A1A]">
Inspelning pågår{interim ? ` — ”${interim}` : "…"}
</span>
)}
</span>
);
}