c3e831b478
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
66 lines
2.0 KiB
TypeScript
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>
|
|
);
|
|
}
|