fix(i18n): fix reference page translation, fix map zoom, connect contact form to server action, fix english 404 links, fix certificate pdf links
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Failing after 1m0s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 6s

This commit is contained in:
2026-05-28 09:58:34 +02:00
parent 2a46015d0d
commit 070f97dd6f
20 changed files with 218 additions and 80 deletions

View File

@@ -8,10 +8,8 @@ import { AnalyticsEvents } from '../analytics/analytics-events';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import dynamic from 'next/dynamic';
import { useTranslations } from 'next-intl';
const AIOrb = dynamic(() => import('./AIOrb'), { ssr: false });
const LOADING_TEXTS = [
'Durchsuche das Kabelhandbuch... 📖',
'Frage den Senior-Ingenieur... 👴🔧',
'Frage ChatGPTs Cousin 2. Grades... 🤖',
];
@@ -51,7 +49,13 @@ export function AISearchResults({
const [error, setError] = useState<string | null>(null);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [copiedAll, setCopiedAll] = useState(false);
const [loadingText, setLoadingText] = useState(LOADING_TEXTS[0]);
const t = useTranslations('AISearch');
// We load texts dynamically from translations
const loadingTexts = t.raw('loadingTexts') as string[];
const prompts = t.raw('prompts') as string[];
const [loadingText, setLoadingText] = useState(loadingTexts[0]);
const inputRef = useRef<HTMLInputElement>(null);
const modalRef = useRef<HTMLDivElement>(null);
@@ -141,11 +145,11 @@ export function AISearchResults({
setTimeout(() => {
setIsLoading(true);
// Start rotating loading texts
let textIdx = Math.floor(Math.random() * LOADING_TEXTS.length);
setLoadingText(LOADING_TEXTS[textIdx]);
let textIdx = Math.floor(Math.random() * loadingTexts.length);
setLoadingText(loadingTexts[textIdx]);
loadingIntervalRef.current = setInterval(() => {
textIdx = (textIdx + 1) % LOADING_TEXTS.length;
setLoadingText(LOADING_TEXTS[textIdx]);
textIdx = (textIdx + 1) % loadingTexts.length;
setLoadingText(loadingTexts[textIdx]);
}, 2500);
}, 400);
@@ -191,8 +195,8 @@ export function AISearchResults({
console.error(err);
const msg =
err.name === 'AbortError'
? 'Anfrage hat zu lange gedauert. Bitte versuche es erneut.'
: err.message || 'Ein Fehler ist aufgetreten.';
? t('timeoutError')
: err.message || t('genericError');
// Show error as a system message in the chat instead of a separate error banner
setMessages((prev) => [
@@ -248,7 +252,7 @@ export function AISearchResults({
const handleCopyChat = () => {
const fullChat = messages
.map((m) => `${m.role === 'user' ? 'Du' : 'Ohm'}:\n${m.content}`)
.map((m) => `${m.role === 'user' ? t('you') : 'Ohm'}:\n${m.content}`)
.join('\n\n');
handleCopy(fullChat);
};
@@ -294,7 +298,7 @@ export function AISearchResults({
<div>
<h2 className="text-white font-bold text-sm tracking-wide">Ohm</h2>
<p className="text-[10px] text-white/30 font-medium tracking-wider uppercase">
{isLoading ? 'Denkt nach...' : error ? 'Fehler aufgetreten' : 'Online'}
{isLoading ? t('thinking') : error ? t('errorStatus') : t('online')}
</p>
</div>
</div>
@@ -303,14 +307,14 @@ export function AISearchResults({
<button
onClick={handleCopyChat}
className="flex items-center gap-1.5 text-[10px] font-bold text-white/40 hover:text-white/80 transition-all duration-200 hover:bg-white/5 rounded-full px-3 py-1.5 cursor-pointer uppercase tracking-wider"
title="gesamten Chat kopieren"
title={t('copyChatTitle')}
>
{copiedAll ? (
<Check className="w-3.5 h-3.5 text-accent" />
) : (
<Copy className="w-3.5 h-3.5" />
)}
<span>{copiedAll ? 'Kopiert' : 'Chat kopieren'}</span>
<span>{copiedAll ? t('copied') : t('copyChat')}</span>
</button>
)}
<button
@@ -336,16 +340,15 @@ export function AISearchResults({
</div>
<div>
<p className="text-xl md:text-2xl font-bold text-white/80">
Wie kann ich helfen?
{t('howCanIHelp')}
</p>
<p className="text-sm text-white/30 mt-2 max-w-md">
Beschreibe dein Projekt, frag nach bestimmten Kabeln, oder nenne mir deine
Anforderungen.
{t('helpDescription')}
</p>
</div>
{/* Quick prompts */}
<div className="flex flex-wrap justify-center gap-2 mt-4">
{['Windpark 33kV Verkabelung', 'NYCWY 4x185', 'Erdkabel für Solarpark'].map(
{prompts.map(
(prompt) => (
<button
key={prompt}
@@ -384,7 +387,7 @@ export function AISearchResults({
? 'top-2 right-2 bg-primary/10 hover:bg-primary/20 text-primary/60 hover:text-primary'
: 'top-2 right-2 bg-white/5 hover:bg-white/10 text-white/40 hover:text-white'
}`}
title="Nachricht kopieren"
title={t('copyMessage')}
>
{copiedIndex === index ? (
<Check className="w-3.5 h-3.5" />
@@ -431,7 +434,7 @@ export function AISearchResults({
{msg.role === 'assistant' && msg.products && msg.products.length > 0 && (
<div className="mt-4 space-y-2 border-t border-white/[0.06] pt-4">
<h4 className="text-[10px] font-bold tracking-widest uppercase text-white/30 mb-2">
Empfohlene Produkte
{t('recommendedProducts')}
</h4>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{msg.products.map((product, idx) => (
@@ -509,7 +512,7 @@ export function AISearchResults({
<AIOrb isThinking={false} hasError={true} />
</div>
<div>
<h3 className="text-sm font-bold text-red-300">Da ist was schiefgelaufen 😬</h3>
<h3 className="text-sm font-bold text-red-300">{t('errorTitle')}</h3>
<p className="text-xs text-red-300/60 mt-1">{error}</p>
<button
onClick={() => {
@@ -519,7 +522,7 @@ export function AISearchResults({
className="flex items-center gap-1.5 text-[10px] font-bold text-red-300/50 hover:text-red-300 mt-2 transition-colors cursor-pointer"
>
<RotateCcw className="w-3 h-3" />
Nochmal versuchen
{t('tryAgain')}
</button>
</div>
</div>
@@ -544,7 +547,7 @@ export function AISearchResults({
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={onKeyDown}
placeholder="Nachricht eingeben..."
placeholder={t('placeholder')}
className="flex-1 bg-transparent border-none text-white text-sm md:text-base px-5 py-4 focus:outline-none placeholder:text-white/20"
disabled={isLoading}
tabIndex={1}
@@ -567,20 +570,20 @@ export function AISearchResults({
? 'bg-accent text-primary shadow-lg shadow-accent/20 hover:shadow-accent/40 hover:scale-105 active:scale-95'
: 'bg-white/5 text-white/20'
}`}
aria-label="Nachricht senden"
aria-label={t('send')}
>
<ArrowUp className="w-4 h-4" strokeWidth={2.5} />
</button>
</div>
<div className="flex items-center justify-center gap-3 mt-2.5">
<span className="text-[9px] uppercase tracking-[0.15em] font-medium text-white/15">
Enter zum Senden · Esc zum Schließen
{t('footerShortcuts')}
</span>
<span className="text-[9px] uppercase tracking-[0.15em] font-medium text-white/15">
·
</span>
<span className="text-[9px] uppercase tracking-[0.15em] font-medium text-accent/40 flex items-center gap-1">
🛡 DSGVO-konform · EU-Server
{t('footerPrivacy')}
</span>
</div>
</div>