webseite sieht gut aus

This commit is contained in:
itsrubberduck
2025-09-14 21:36:45 +02:00
parent d5737b62fc
commit 5f78847d88
7 changed files with 326 additions and 147 deletions

Binary file not shown.

View File

@@ -1,22 +1,22 @@
<template>
<div class="bg-[#0b1020] text-white antialiased selection:bg-cyan-400/30">
<!-- NAV -->
<header class="sticky top-0 z-50 bg-[#0b1020]/70 backdrop-blur border-b border-white/10">
<nav class="mx-auto max-w-[1200px] px-4 py-3 flex items-center justify-between">
<a href="#" class="flex items-center gap-2 font-semibold tracking-tight">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" class="text-cyan-400"><path d="M12 3l8 4v10l-8 4-8-4V7l8-4z" stroke="currentColor" stroke-width="1.5"/><path d="M12 7l4 2v6l-4 2-4-2V9l4-2z" fill="currentColor" class="opacity-60"/></svg>
<header class="sticky top-0 z-50 bg-[#0b1020]/70 backdrop-blur border-b border-white/10" data-aos="fade-down">
<nav class="container-outer py-3 flex items-center justify-between">
<NuxtLink to="#" class="flex items-center gap-2 font-semibold tracking-tight">
<v-icon icon="mdi-radar" size="28" class="text-cyan-400" />
<span class="text-white">OpenSquawk</span>
</a>
</NuxtLink>
<div class="hidden md:flex gap-6 text-sm">
<a href="#features" class="hover:text-cyan-300">Features</a>
<a href="#learn" class="hover:text-cyan-300">Lernpfad</a>
<a href="#pricing" class="hover:text-cyan-300">Preise</a>
<a href="#opensource" class="hover:text-cyan-300">OpenSource</a>
<a href="#faq" class="hover:text-cyan-300">FAQ</a>
<NuxtLink to="#features" class="hover:text-cyan-300">Features</NuxtLink>
<NuxtLink to="#learn" class="hover:text-cyan-300">Lernpfad</NuxtLink>
<NuxtLink to="#pricing" class="hover:text-cyan-300">Preise</NuxtLink>
<NuxtLink to="#opensource" class="hover:text-cyan-300">OpenSource</NuxtLink>
<NuxtLink to="#faq" class="hover:text-cyan-300">FAQ</NuxtLink>
</div>
<div class="flex items-center gap-2">
<a href="#demo" class="btn btn-ghost text-sm">LiveDemo</a>
<a href="#cta" class="btn btn-primary text-sm">Frühzugang</a>
<NuxtLink to="#demo" class="btn btn-ghost text-sm">LiveDemo</NuxtLink>
<NuxtLink to="#cta" class="btn btn-primary text-sm">Frühzugang</NuxtLink>
</div>
</nav>
</header>
@@ -27,27 +27,27 @@
<div class="absolute -top-24 right-0 w-[600px] h-[600px] bg-cyan-500/10 rounded-full blur-3xl" />
</div>
<div class="mx-auto max-w-[1200px] px-4 pt-16 pb-12 md:pt-24 md:pb-20">
<div class="max-w-2xl">
<div class="container-outer pt-16 pb-12 md:pt-24 md:pb-20">
<div class="max-w-2xl" data-aos="fade-up">
<span class="chip mb-4">OpenSource · AIATC · Simulator</span>
<h1 class="text-4xl md:text-6xl font-semibold leading-tight">OpenSquawk<br><span class="text-cyan-400">AIATC, offen & bezahlbar</span></h1>
<h1 class="text-4xl md:text-6xl font-semibold leading-tight">OpenSquawk<br/><span class="text-cyan-400">AIATC, offen & bezahlbar</span></h1>
<p class="mt-4 md:mt-6 text-white/80 text-base md:text-lg">Offene Alternative für AIATC im Flugsimulator. Lerne echte Phraseologie, übe sichere Abläufe und steige Schritt für Schritt Richtung VATSIM ein. Optional als gehosteter Plan so günstig wie möglich.</p>
<div class="flex flex-col sm:flex-row gap-3 mt-6">
<a href="#cta" class="btn btn-primary text-base">Frühzugang sichern</a>
<a href="#video" class="btn btn-ghost text-base">1MinÜberblick ansehen</a>
<NuxtLink to="#cta" class="btn btn-primary text-base">Frühzugang sichern</NuxtLink>
<NuxtLink to="#video" class="btn btn-ghost text-base">1MinÜberblick ansehen</NuxtLink>
</div>
<div class="mt-6 flex items-center gap-4 text-white/70 text-sm">
<div class="flex -space-x-3">
<img alt="avatar" src="https://i.pravatar.cc/40?img=1" class="w-8 h-8 rounded-full border border-white/10" />
<img alt="avatar" src="https://i.pravatar.cc/40?img=2" class="w-8 h-8 rounded-full border border-white/10" />
<img alt="avatar" src="https://i.pravatar.cc/40?img=3" class="w-8 h-8 rounded-full border border-white/10" />
<img alt="avatar" src="https://i.pravatar.cc/40?img=1" class="w-8 h-8 rounded-full border border-white/10"/>
<img alt="avatar" src="https://i.pravatar.cc/40?img=2" class="w-8 h-8 rounded-full border border-white/10"/>
<img alt="avatar" src="https://i.pravatar.cc/40?img=3" class="w-8 h-8 rounded-full border border-white/10"/>
</div>
<p><b>1500+</b> Pilot:innen im EarlyAccess</p>
</div>
</div>
<div class="mt-10 md:mt-16">
<div class="mt-10 md:mt-16" data-aos="zoom-in" data-aos-delay="100">
<div class="card relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/5 to-transparent" />
<div class="absolute inset-0 bg-gradient-to-r from-cyan-500/10 via-blue-500/5 to-transparent"></div>
<img src="https://images.unsplash.com/photo-1509749837427-392dfae2dbd1?q=80&w=1600&auto=format&fit=crop" alt="Cockpit" class="rounded-xl w-full object-cover" />
<div class="absolute bottom-3 right-3 text-xs text-white/70 bg-black/40 px-2 py-1 rounded">Symbolbild</div>
</div>
@@ -56,43 +56,43 @@
</section>
<!-- SOCIAL PROOF / LOGOS -->
<section class="py-10 border-t border-white/10 bg-[#0a0f1c]">
<div class="mx-auto max-w-[1200px] px-4">
<section class="py-10 border-t border-white/10 bg-[#0a0f1c]" data-aos="fade-up">
<div class="container-outer">
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 items-center opacity-80">
<div class="text-center text-white/60">MSFS</div>
<div class="text-center text-white/60">XPlane</div>
<div class="text-center text-white/60">VATSIM* (Lernpfad)</div>
<div class="text-center text-white/60">IVAO* (Lernpfad)</div>
<div class="flex items-center justify-center gap-2 text-white/60"><v-icon icon="mdi-microsoft-xbox" class="opacity-70"/>MSFS</div>
<div class="flex items-center justify-center gap-2 text-white/60"><v-icon icon="mdi-airplane" class="opacity-70"/>XPlane</div>
<div class="flex items-center justify-center gap-2 text-white/60"><v-icon icon="mdi-headset" class="opacity-70"/>VATSIM* (Lernpfad)</div>
<div class="flex items-center justify-center gap-2 text-white/60"><v-icon icon="mdi-radar" class="opacity-70"/>IVAO* (Lernpfad)</div>
</div>
</div>
</section>
<!-- FEATURES -->
<section id="features" class="py-16 md:py-24 bg-gradient-to-b from-[#0a0f1c] to-[#0b1020]">
<div class="mx-auto max-w-[1200px] px-4">
<div class="max-w-2xl mb-10">
<div class="container-outer">
<div class="max-w-2xl mb-10" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-semibold">Warum OpenSquawk?</h2>
<p class="mt-3 text-white/80">EchtzeitFunk mit KI, lernfreundliche Erklärungen und offene Architektur damit du souverän taxi, departure, approach & landing beherrschst.</p>
</div>
<div class="grid md:grid-cols-3 gap-6">
<div class="card">
<div class="card" data-aos="fade-up" data-aos-delay="0">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-cyan-500/20 border border-cyan-400/20"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M12 3l8 4v10l-8 4-8-4V7l8-4z" stroke="#22d3ee"/></svg></div>
<div class="p-2 rounded-lg bg-cyan-500/20 border border-cyan-400/20"><v-icon icon="mdi-tower-fire" class="text-cyan-300"/></div>
<h3 class="font-semibold text-lg">AIATC in Echtzeit</h3>
</div>
<p class="mt-3 text-white/80">StreamingASR, LLMVerständnis & TTS Stimmen. Klar, schnell, latenzarm optimiert für Phraseologie.</p>
</div>
<div class="card">
<div class="card" data-aos="fade-up" data-aos-delay="100">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-cyan-500/20 border border-cyan-400/20"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="#22d3ee"/></svg></div>
<div class="p-2 rounded-lg bg-cyan-500/20 border border-cyan-400/20"><v-icon icon="mdi-school" class="text-cyan-300"/></div>
<h3 class="font-semibold text-lg">Lernpfad VATSIM</h3>
</div>
<p class="mt-3 text-white/80">Geführte Übungen, Checks & Erklärungen vom Readback bis zu komplexen Clearances. Schrittweise Richtung OnlineNetzwerke.</p>
</div>
<div class="card">
<div class="card" data-aos="fade-up" data-aos-delay="200">
<div class="flex items-center gap-3">
<div class="p-2 rounded-lg bg-cyan-500/20 border border-cyan-400/20"><svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M5 12h14M12 5v14" stroke="#22d3ee"/></svg></div>
<div class="p-2 rounded-lg bg-cyan-500/20 border border-cyan-400/20"><v-icon icon="mdi-source-repository" class="text-cyan-300"/></div>
<h3 class="font-semibold text-lg">OpenSource + Hosted</h3>
</div>
<p class="mt-3 text-white/80">Selbst hosten oder unseren günstigen Plan nutzen. Transparente Architektur, CommunityPlugins & APIs.</p>
@@ -100,7 +100,7 @@
</div>
<div class="grid md:grid-cols-2 gap-6 mt-6">
<div class="card">
<div class="card" data-aos="fade-up" data-aos-delay="0">
<h3 class="font-semibold text-lg">TaxiRouten & Ground Awareness</h3>
<p class="mt-3 text-white/80">OSM/apt.datbasierte TaxiwayGraphen, A*Routing zu Holding Points, progressive Anweisungen mit VisualOverlay.</p>
<ul class="mt-3 space-y-2 text-white/70 text-sm list-disc list-inside">
@@ -109,14 +109,14 @@
<li>Optional: NOTAM/ATIS Einbindung</li>
</ul>
</div>
<div class="card">
<div class="card" data-aos="fade-up" data-aos-delay="100">
<h3 class="font-semibold text-lg">TechStack</h3>
<p class="mt-3 text-white/80">StreamingASR, LLMDialog, TTS orchestriert mit WebRTC/WS. OfflineFallbacks möglich.</p>
<div class="mt-4 grid grid-cols-2 gap-3 text-sm">
<div class="glass rounded-xl p-3">ASR: Whisper/Alternativen (API)</div>
<div class="glass rounded-xl p-3">LLM: GPT5Nano/4oMini etc.</div>
<div class="glass rounded-xl p-3">TTS: neural voices</div>
<div class="glass rounded-xl p-3">Routing: A* / Dijkstra</div>
<div class="glass rounded-xl p-3 flex items-center gap-2"><v-icon icon="mdi-waveform"/>ASR: Whisper/Alternativen</div>
<div class="glass rounded-xl p-3 flex items-center gap-2"><v-icon icon="mdi-robot"/>LLM: GPT5Nano/4oMini</div>
<div class="glass rounded-xl p-3 flex items-center gap-2"><v-icon icon="mdi-microphone"/>TTS: neural voices</div>
<div class="glass rounded-xl p-3 flex items-center gap-2"><v-icon icon="mdi-routes"/>Routing: A* / Dijkstra</div>
</div>
</div>
</div>
@@ -125,9 +125,9 @@
<!-- LEARN PATH -->
<section id="learn" class="py-16 md:py-24 bg-[#0b1020]">
<div class="mx-auto max-w-[1200px] px-4">
<div class="container-outer">
<div class="grid md:grid-cols-2 gap-8 items-center">
<div>
<div data-aos="fade-right">
<h2 class="text-3xl md:text-4xl font-semibold">ATC verstehen Schritt für Schritt</h2>
<p class="mt-3 text-white/80">Geführter Lernmodus: Hören, Nachsprechen, Feedback. Vom ersten Ready to taxi bis zur komplexen STAR/ApproachFreigabe.</p>
<ol class="mt-5 space-y-3 text-white/80">
@@ -138,11 +138,11 @@
<li class="flex gap-3"><span class="chip">5</span><span><b>VATSIM</b>: Checklisten, Etiquette, LiveÜbungen.</span></li>
</ol>
<div class="mt-6 flex gap-3">
<a href="#cta" class="btn btn-primary">Jetzt loslegen</a>
<a href="#faq" class="btn btn-ghost">FAQ</a>
<NuxtLink to="#cta" class="btn btn-primary">Jetzt loslegen</NuxtLink>
<NuxtLink to="#faq" class="btn btn-ghost">FAQ</NuxtLink>
</div>
</div>
<div class="card">
<div class="card" data-aos="fade-left">
<ClientOnly>
<video id="video" class="w-full rounded-xl" autoplay muted loop playsinline poster="https://images.unsplash.com/photo-1518306724291-1f5c9b4d2452?q=80&w=1600&auto=format&fit=crop">
<source src="https://cdn.coverr.co/videos/coverr-airplane-taking-off-8255/1080p.mp4" type="video/mp4" />
@@ -156,8 +156,8 @@
<!-- PRICING -->
<section id="pricing" class="py-16 md:py-24 bg-gradient-to-b from-[#0b1020] to-[#0a0f1c]">
<div class="mx-auto max-w-[1200px] px-4">
<div class="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
<div class="container-outer">
<div class="flex flex-col md:flex-row md:items-end md:justify-between gap-4" data-aos="fade-up">
<div class="max-w-2xl">
<h2 class="text-3xl md:text-4xl font-semibold">Preise</h2>
<p class="mt-3 text-white/80">OpenSource bleibt frei. Der gehostete Plan ist so günstig wie möglich kalkuliert. Umschalten:
@@ -168,7 +168,7 @@
<div class="mt-8 grid md:grid-cols-3 gap-6">
<!-- OSS -->
<div class="card relative">
<div class="card relative" data-aos="fade-up" data-aos-delay="0">
<div class="absolute -top-3 right-4 chip">Community</div>
<h3 class="text-xl font-semibold">OpenSource (Selfhost)</h3>
<p class="mt-2 text-white/80">Volle Kontrolle. Eigene Infrastruktur. APIKeys selbst verwalten.</p>
@@ -178,10 +178,10 @@
<li> Plugins & SDK</li>
<li> CommunitySupport</li>
</ul>
<a href="#opensource" class="btn btn-ghost w-full mt-6">Repository ansehen</a>
<NuxtLink to="#opensource" class="btn btn-ghost w-full mt-6">Repository ansehen</NuxtLink>
</div>
<!-- Hosted Basic -->
<div class="card border-2 border-cyan-400/40 relative shadow-glow">
<div class="card border-2 border-cyan-400/40 relative shadow-[0_0_40px_rgba(34,211,238,.25)]" data-aos="fade-up" data-aos-delay="100">
<div class="absolute -top-3 right-4 chip bg-cyan-500/30 border-cyan-400/50">Empfohlen</div>
<h3 class="text-xl font-semibold">Hosted Basic</h3>
<p class="mt-2 text-white/80">Alles fertig eingerichtet. Ideal zum Lernen & Üben.</p>
@@ -191,11 +191,11 @@
<li> Lernpfad & Fortschritt</li>
<li> Updates & CloudScaling</li>
</ul>
<a href="#cta" class="btn btn-primary w-full mt-6">Kostenlos testen</a>
<NuxtLink to="#cta" class="btn btn-primary w-full mt-6">Kostenlos testen</NuxtLink>
<p class="mt-3 text-xs text-white/60">Preisziel: minimal + Betriebskosten. Änderungen möglich.</p>
</div>
<!-- Hosted Pro -->
<div class="card relative">
<div class="card relative" data-aos="fade-up" data-aos-delay="200">
<div class="absolute -top-3 right-4 chip">PowerUser</div>
<h3 class="text-xl font-semibold">Hosted Pro</h3>
<p class="mt-2 text-white/80">Mehr Kontingente, eigene Stimmen, APIZugriff.</p>
@@ -205,7 +205,7 @@
<li> TeamSeats</li>
<li> Priorisierter Support</li>
</ul>
<a href="#cta" class="btn btn-ghost w-full mt-6">Kontakt aufnehmen</a>
<NuxtLink to="#cta" class="btn btn-ghost w-full mt-6">Kontakt aufnehmen</NuxtLink>
</div>
</div>
</div>
@@ -213,23 +213,23 @@
<!-- OPEN SOURCE -->
<section id="opensource" class="py-16 md:py-24 bg-[#0a0f1c]">
<div class="mx-auto max-w-[1200px] px-4">
<div class="container-outer">
<div class="grid md:grid-cols-2 gap-8 items-center">
<div>
<div data-aos="fade-right">
<h2 class="text-3xl md:text-4xl font-semibold">OpenSource, Communitygetrieben</h2>
<p class="mt-3 text-white/80">Transparente Architektur, klare Roadmap, offene Issues. Baue eigene Voices, Integrationen und Workflows.</p>
<ul class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<li class="glass rounded-xl p-3">MIT/ApacheLizenz (tbd)</li>
<li class="glass rounded-xl p-3">DockerCompose / Helm</li>
<li class="glass rounded-xl p-3">PluginSDK (TS/JS)</li>
<li class="glass rounded-xl p-3">CLI & REST API</li>
<li class="glass rounded-xl p-3 flex items-center gap-2"><v-icon icon="mdi-license"/>MIT/ApacheLizenz (tbd)</li>
<li class="glass rounded-xl p-3 flex items-center gap-2"><v-icon icon="mdi-docker"/>DockerCompose / Helm</li>
<li class="glass rounded-xl p-3 flex items-center gap-2"><v-icon icon="mdi-puzzle"/>PluginSDK (TS/JS)</li>
<li class="glass rounded-xl p-3 flex items-center gap-2"><v-icon icon="mdi-console"/>CLI & REST API</li>
</ul>
<div class="mt-6 flex gap-3">
<a href="#cta" class="btn btn-primary">Mitmachen</a>
<a href="#demo" class="btn btn-ghost">Demo</a>
<NuxtLink to="#cta" class="btn btn-primary">Mitmachen</NuxtLink>
<NuxtLink to="#demo" class="btn btn-ghost">Demo</NuxtLink>
</div>
</div>
<div class="card">
<div class="card" data-aos="fade-left">
<pre class="text-xs md:text-sm overflow-x-auto"><code>// Beispiel: RESTRoute für TaxiRouten
POST /api/route/taxi
{
@@ -245,24 +245,24 @@ POST /api/route/taxi
<!-- HOW IT WORKS -->
<section class="py-16 md:py-24 bg-[#0b1020]">
<div class="mx-auto max-w-[1200px] px-4">
<div class="max-w-2xl mb-10">
<div class="container-outer">
<div class="max-w-2xl mb-10" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-semibold">So funktionierts</h2>
<p class="mt-3 text-white/80">Audio rein Verständnis Antwort raus. Designed für niedrige Latenz & klare FunkDisziplin.</p>
</div>
<div class="grid md:grid-cols-4 gap-4 md:gap-6">
<div class="card"><h3 class="font-semibold">1 · ASR</h3><p class="mt-2 text-white/80">StreamingSpeechtoText mit FunkTuning.</p></div>
<div class="card"><h3 class="font-semibold">2 · NLU</h3><p class="mt-2 text-white/80">LLM versteht Intention, Kontext, State.</p></div>
<div class="card"><h3 class="font-semibold">3 · Logic</h3><p class="mt-2 text-white/80">Regeln, Flugdaten, TaxiRouting, Validierung.</p></div>
<div class="card"><h3 class="font-semibold">4 · TTS</h3><p class="mt-2 text-white/80">Natürliches VoiceOut mit korrekten Zahlen.</p></div>
<div class="card" data-aos="fade-up" data-aos-delay="0"><h3 class="font-semibold flex items-center gap-2"><v-icon icon="mdi-waveform"/>1 · ASR</h3><p class="mt-2 text-white/80">StreamingSpeechtoText mit FunkTuning.</p></div>
<div class="card" data-aos="fade-up" data-aos-delay="100"><h3 class="font-semibold flex items-center gap-2"><v-icon icon="mdi-brain"/>2 · NLU</h3><p class="mt-2 text-white/80">LLM versteht Intention, Kontext, State.</p></div>
<div class="card" data-aos="fade-up" data-aos-delay="200"><h3 class="font-semibold flex items-center gap-2"><v-icon icon="mdi-logic-gate-and"/>3 · Logic</h3><p class="mt-2 text-white/80">Regeln, Flugdaten, TaxiRouting, Validierung.</p></div>
<div class="card" data-aos="fade-up" data-aos-delay="300"><h3 class="font-semibold flex items-center gap-2"><v-icon icon="mdi-microphone"/>4 · TTS</h3><p class="mt-2 text-white/80">Natürliches VoiceOut mit korrekten Zahlen.</p></div>
</div>
</div>
</section>
<!-- CTA -->
<section id="cta" class="py-16 md:py-24 bg-gradient-to-tr from-cyan-500/20 via-blue-500/10 to-transparent border-y border-white/10">
<div class="mx-auto max-w-[1200px] px-4">
<div class="card md:flex md:items-center md:justify-between">
<div class="container-outer">
<div class="card md:flex md:items-center md:justify-between" data-aos="zoom-in">
<div>
<h3 class="text-2xl md:text-3xl font-semibold">Jetzt OpenSquawk ausprobieren</h3>
<p class="mt-2 text-white/80">Starte kostenlos. Upgrade jederzeit. Keine Bindung.</p>
@@ -272,31 +272,31 @@ POST /api/route/taxi
<button class="btn btn-primary">Einladung anfordern</button>
</form>
</div>
<p v-if="thanks" class="mt-3 text-sm text-green-300">Danke! Wir melden uns in Kürze mit deinem Zugang.</p>
<p v-if="thanks" class="mt-3 text-sm text-green-300" data-aos="fade-in">Danke! Wir melden uns in Kürze mit deinem Zugang.</p>
</div>
</section>
<!-- FAQ -->
<section id="faq" class="py-16 md:py-24 bg-[#0a0f1c]">
<div class="mx-auto max-w-[1200px] px-4">
<div class="max-w-2xl mb-10">
<div class="container-outer">
<div class="max-w-2xl mb-10" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-semibold">FAQ</h2>
<p class="mt-3 text-white/80">Kurz beantwortet.</p>
</div>
<div class="grid md:grid-cols-2 gap-6">
<div class="card">
<div class="card" data-aos="fade-up" data-aos-delay="0">
<h3 class="font-semibold">Ist das für reale Luftfahrt?</h3>
<p class="mt-2 text-white/80">Nein, OpenSquawk ist für Flugsimulatoren und Training. Nicht für den realen Flugfunk.</p>
</div>
<div class="card">
<div class="card" data-aos="fade-up" data-aos-delay="100">
<h3 class="font-semibold">Welche Simulatoren werden unterstützt?</h3>
<p class="mt-2 text-white/80">MSFS, XPlane (weitere geplant). Integrationen per PluginSDK.</p>
</div>
<div class="card">
<div class="card" data-aos="fade-up" data-aos-delay="200">
<h3 class="font-semibold">Kann ich selbst hosten?</h3>
<p class="mt-2 text-white/80">Ja. DockerCompose/Helm bereitgestellt. HostedPlan als bequeme Alternative.</p>
</div>
<div class="card">
<div class="card" data-aos="fade-up" data-aos-delay="300">
<h3 class="font-semibold">Wie günstig ist Hosted Basic?</h3>
<p class="mt-2 text-white/80">Ziel: knapp über Betriebskosten. Pilotphase mit 45 mtl. (vorläufig).</p>
</div>
@@ -305,35 +305,35 @@ POST /api/route/taxi
</section>
<!-- FOOTER -->
<footer class="py-12 bg-[#0b1020] border-t border-white/10">
<div class="mx-auto max-w-[1200px] px-4">
<footer class="py-12 bg-[#0b1020] border-t border-white/10" data-aos="fade-up">
<div class="container-outer">
<div class="grid md:grid-cols-4 gap-6">
<div>
<div class="flex items-center gap-2 font-semibold"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" class="text-cyan-400"><path d="M12 3l8 4v10l-8 4-8-4V7l8-4z" stroke="currentColor"/></svg>OpenSquawk</div>
<div class="flex items-center gap-2 font-semibold"><v-icon icon="mdi-radar" class="text-cyan-400"/>OpenSquawk</div>
<p class="mt-3 text-white/70 text-sm">OpenSource AIATC für Simulatorpiloten. Lernpfad & günstiger HostedPlan.</p>
</div>
<div>
<h4 class="font-semibold mb-3">Produkt</h4>
<ul class="space-y-2 text-white/70 text-sm">
<li><a href="#features" class="hover:text-cyan-300">Features</a></li>
<li><a href="#learn" class="hover:text-cyan-300">Lernpfad</a></li>
<li><a href="#pricing" class="hover:text-cyan-300">Preise</a></li>
<li><NuxtLink to="#features" class="hover:text-cyan-300">Features</NuxtLink></li>
<li><NuxtLink to="#learn" class="hover:text-cyan-300">Lernpfad</NuxtLink></li>
<li><NuxtLink to="#pricing" class="hover:text-cyan-300">Preise</NuxtLink></li>
</ul>
</div>
<div>
<h4 class="font-semibold mb-3">Ressourcen</h4>
<ul class="space-y-2 text-white/70 text-sm">
<li><a href="#opensource" class="hover:text-cyan-300">OpenSource</a></li>
<li><a href="#demo" class="hover:text-cyan-300">Demo</a></li>
<li><a href="#faq" class="hover:text-cyan-300">FAQ</a></li>
<li><NuxtLink to="#opensource" class="hover:text-cyan-300">OpenSource</NuxtLink></li>
<li><NuxtLink to="#demo" class="hover:text-cyan-300">Demo</NuxtLink></li>
<li><NuxtLink to="#faq" class="hover:text-cyan-300">FAQ</NuxtLink></li>
</ul>
</div>
<div>
<h4 class="font-semibold mb-3">Rechtliches</h4>
<ul class="space-y-2 text-white/70 text-sm">
<li><a href="#" class="hover:text-cyan-300">Impressum</a></li>
<li><a href="#" class="hover:text-cyan-300">Datenschutz</a></li>
<li><a href="#" class="hover:text-cyan-300">Nutzungshinweise</a></li>
<li><NuxtLink to="#" class="hover:text-cyan-300">Impressum</NuxtLink></li>
<li><NuxtLink to="#" class="hover:text-cyan-300">Datenschutz</NuxtLink></li>
<li><NuxtLink to="#" class="hover:text-cyan-300">Nutzungshinweise</NuxtLink></li>
</ul>
</div>
</div>
@@ -344,7 +344,7 @@ POST /api/route/taxi
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { ref, onMounted } from 'vue'
import { useHead } from '#imports'
const yearly = ref(true)
@@ -371,43 +371,33 @@ useHead({
{ name: 'twitter:title', content: 'OpenSquawk OpenSource AIATC' },
{ name: 'twitter:description', content: 'Open, günstig, lernfreundlich. AIATC mit Lernpfad & gehostetem Plan.' },
{ name: 'twitter:image', content: 'https://opensquawk.example.com/cover.png' }
],
script: [
{ src: 'https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js', defer: true },
{ src: 'https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/ScrollTrigger.min.js', defer: true },
{ type: 'application/ld+json', children: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: 'OpenSquawk',
applicationCategory: 'Simulation',
offers: { '@type': 'AggregateOffer', lowPrice: '0', priceCurrency: 'EUR' },
description: 'OpenSource AIATC für Flugsimulatoren mit Lernpfad & HostedPlan.',
operatingSystem: 'Windows, macOS'
}) }
]
})
onMounted(() => {
const w: any = window as any
const gsap = w.gsap
const ScrollTrigger = w.ScrollTrigger
if (!gsap || !ScrollTrigger) return
gsap.registerPlugin(ScrollTrigger)
gsap.from('header nav', { y: -30, opacity: 0, duration: .6, ease: 'power2'})
gsap.from('.card', { opacity: 0, y: 20, duration: .6, stagger: .08, ease: 'power2', scrollTrigger: { trigger: '#features', start: 'top 80%' }})
gsap.from('#pricing .card', { opacity: 0, y: 24, duration: .6, stagger: .08, ease: 'power2', scrollTrigger: { trigger: '#pricing', start: 'top 80%' }})
gsap.from('#learn .card, #learn h2, #learn ol li', { opacity: 0, y: 18, duration: .6, stagger: .06, ease: 'power2', scrollTrigger: { trigger: '#learn', start: 'top 80%' }})
gsap.from('#opensource .card', { opacity: 0, y: 18, duration: .6, stagger: .06, ease: 'power2', scrollTrigger: { trigger: '#opensource', start: 'top 80%' }})
gsap.from('#cta .card', { opacity: 0, scale: .98, duration: .6, ease: 'power2', scrollTrigger: { trigger: '#cta', start: 'top 90%' }})
// AOS: falls du kein NuxtAOS Modul nutzt, hier Fallback
onMounted(async () => {
// @ts-ignore optionaler Fallback
if (!('AOS' in window)) {
const [{ default: AOS }] = await Promise.all([
import('aos'),
import('aos/dist/aos.css')
])
AOS.init({ once: true, duration: 600, easing: 'ease-out' })
}
})
</script>
<style scoped>
.container-outer { @apply mx-auto max-w-screen-xl px-4; }
.gradient-hero {
background: radial-gradient(1200px 600px at 10% -10%, rgba(6,182,212,.35), transparent),
radial-gradient(900px 480px at 100% 10%, rgba(59,130,246,.25), transparent),
linear-gradient(180deg, #0b1020 0%, #0b1020 60%, #0a0f1c 100%);
}
.glass { background: rgba(255,255,255,.06); backdrop-filter: blur(10px); border: 1px solid rgba(255,255,255,.08); }
.card { @apply glass rounded-2xl p-5 md:p-6; }
.btn { @apply inline-flex items-center justify-center gap-2 rounded-xl px-5 py-3 font-medium transition; }
.btn-primary { @apply bg-cyan-500 text-white hover:bg-cyan-400 shadow-[0_0_40px_rgba(34,211,238,.25)]; }
.btn-ghost { @apply bg-white/5 text-white hover:bg-white/10; }
.card { @apply glass rounded-2xl p-5 md:p-6; }
.chip { @apply inline-flex items-center gap-2 rounded-full bg-white/10 border border-white/15 text-white px-3 py-1 text-xs; }
.gradient-hero { background: radial-gradient(1200px 600px at 10% -10%, rgba(6,182,212,.35), transparent), radial-gradient(900px 480px at 100% 10%, rgba(59,130,246,.25), transparent), linear-gradient(180deg, #0b1020 0%, #0b1020 60%, #0a0f1c 100%); }
</style>

View File

@@ -2,5 +2,6 @@
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
modules: ['vuetify-nuxt-module', '@nuxtjs/tailwindcss', 'nuxt-aos']
})
modules: ['vuetify-nuxt-module', '@nuxtjs/tailwindcss', 'nuxt-aos'],
aos: { once: true, duration: 600, easing: 'ease-out' }
})

View File

@@ -14,7 +14,7 @@
"dotenv": "^17.2.2",
"nuxt": "^4.1.1",
"nuxt-aos": "1.2.5",
"openai": "^5.20.2",
"openai": "^4.66.0",
"vue": "^3.5.21",
"vue-router": "^4.5.1",
"vuetify": "3.9.0-beta.1",

View File

@@ -1,35 +1,62 @@
import { readMultipartFormData, createError } from "h3";
import { openai, LLM_MODEL, TTS_MODEL, atcReplyPrompt } from "../../utils/openai";
import { LLM_MODEL, TTS_MODEL, atcReplyPrompt, openai } from "../../utils/openai";
import { applyRadioEffect } from "../../utils/radio";
import { writeFile, rm } from "node:fs/promises";
import { writeFile, rm, readFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { toFile } from "openai/uploads"; // ⟵ NEU
export default defineEventHandler(async (event) => {
const parts = await readMultipartFormData(event);
const mode = parts?.find(p=>p.name==="mode")?.data?.toString("utf8") || "text";
const audio = parts?.find(p=>p.type && p.data);
if (!audio) throw createError({ statusCode:400, statusMessage:"No audio" });
if (!parts) throw createError({ statusCode: 400, statusMessage: "No form-data" });
const file = new File([audio.data], audio.filename || "audio.webm", { type: audio.type || "audio/webm" });
const mode = (parts.find(p => p.name === "mode")?.data?.toString("utf8") || "text").toLowerCase();
const audio = parts.find(p => p.type && p.data);
if (!audio) throw createError({ statusCode: 400, statusMessage: "No audio file" });
// ✅ saubere Datei-Erzeugung
const file = await toFile(
audio.data,
audio.filename || "ptt.webm",
{ type: audio.type || "audio/webm" }
);
// 1) Transkribieren
const tr = await openai.audio.transcriptions.create({ model: "whisper-1", file });
const pilotText = tr.text.trim();
const pilotText = tr.text?.trim() || "";
if (!pilotText) throw createError({ statusCode: 400, statusMessage: "Empty transcription" });
const resp = await openai.responses.create({ model: LLM_MODEL, input: atcReplyPrompt(pilotText) });
// 2) ATC-Antwort
const resp = await openai.responses.create({
model: LLM_MODEL,
input: atcReplyPrompt(pilotText),
});
const replyText = resp.output_text?.trim() || "";
if (!replyText) throw createError({ statusCode: 500, statusMessage: "LLM empty" });
// 3) Optional TTS + Funk
if (mode === "tts") {
const clean = join(tmpdir(), `tts-${randomUUID()}.wav`);
const radio = join(tmpdir(), `radio-${randomUUID()}.wav`);
const tts = await openai.audio.speech.create({ model: TTS_MODEL, voice: "alloy", format: "wav", input: replyText });
const tts = await openai.audio.speech.create({
model: TTS_MODEL,
voice: "alloy",
format: "wav",
input: replyText,
});
await writeFile(clean, Buffer.from(await tts.arrayBuffer()));
await applyRadioEffect(clean, radio);
const data = await import("node:fs/promises").then(fs => fs.readFile(radio));
await rm(clean).catch(()=>{});
const data = await readFile(radio);
const b64 = Buffer.from(data).toString("base64");
await rm(radio).catch(()=>{});
return { pilotText, replyText, audio:{ mime:"audio/wav", base64:b64 } };
rm(clean).catch(() => {});
rm(radio).catch(() => {});
return { pilotText, replyText, audio: { mime: "audio/wav", base64: b64 } };
}
return { pilotText, replyText };
});

View File

@@ -1,10 +1,24 @@
import { readMultipartFormData, createError } from "h3";
import { openai } from "../../utils/openai";
import { toFile } from "openai/uploads"; // ⟵ NEU
export default defineEventHandler(async (event) => {
const parts = await readMultipartFormData(event);
const audio = parts?.find(p=>p.type && p.data);
if (!audio) throw createError({ statusCode:400, statusMessage:"No audio" });
const file = new File([audio.data], audio.filename || "audio.webm", { type: audio.type || "audio/webm" });
const tr = await openai.audio.transcriptions.create({ model: "whisper-1", file });
if (!parts) throw createError({ statusCode: 400, statusMessage: "No form-data" });
const audio = parts.find(p => p.type && p.data);
if (!audio) throw createError({ statusCode: 400, statusMessage: "No audio file" });
const file = await toFile(
audio.data, // Buffer
audio.filename || "ptt.webm", // ⟵ mit Endung
{ type: audio.type || "audio/webm" } // ⟵ korrekter MIME
);
const tr = await openai.audio.transcriptions.create({
model: "whisper-1",
file
});
return { text: tr.text };
});

163
yarn.lock
View File

@@ -2059,6 +2059,34 @@ __metadata:
languageName: node
linkType: hard
"@types/node-fetch@npm:^2.6.4":
version: 2.6.13
resolution: "@types/node-fetch@npm:2.6.13"
dependencies:
"@types/node": "npm:*"
form-data: "npm:^4.0.4"
checksum: 10c0/6313c89f62c50bd0513a6839cdff0a06727ac5495ccbb2eeda51bb2bbbc4f3c0a76c0393a491b7610af703d3d2deb6cf60e37e59c81ceeca803ffde745dbf309
languageName: node
linkType: hard
"@types/node@npm:*":
version: 24.4.0
resolution: "@types/node@npm:24.4.0"
dependencies:
undici-types: "npm:~7.11.0"
checksum: 10c0/3aefa4c6b006b3478f2d28a48d830ab7350ce24efdaf352011418e3ee17ed7683ca9c0a1840e770685ce78f413344badafa91e034be02488efe2eb6b612bd542
languageName: node
linkType: hard
"@types/node@npm:^18.11.18":
version: 18.19.124
resolution: "@types/node@npm:18.19.124"
dependencies:
undici-types: "npm:~5.26.4"
checksum: 10c0/a1b1aa2d21a0b0dec473b9b15e048a25f86cd26f7fc1b9036b43b838ee50b33534518c503f12c9a86be4cebb0f957fd3583fee9c78372ece973a1081c9fc7966
languageName: node
linkType: hard
"@types/parse-path@npm:^7.0.0":
version: 7.0.3
resolution: "@types/parse-path@npm:7.0.3"
@@ -2454,6 +2482,15 @@ __metadata:
languageName: node
linkType: hard
"agentkeepalive@npm:^4.2.1":
version: 4.6.0
resolution: "agentkeepalive@npm:4.6.0"
dependencies:
humanize-ms: "npm:^1.2.1"
checksum: 10c0/235c182432f75046835b05f239708107138a40103deee23b6a08caee5136873709155753b394ec212e49e60e94a378189562cb01347765515cff61b692c69187
languageName: node
linkType: hard
"alien-signals@npm:^2.0.5":
version: 2.0.7
resolution: "alien-signals@npm:2.0.7"
@@ -2597,6 +2634,13 @@ __metadata:
languageName: node
linkType: hard
"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d
languageName: node
linkType: hard
"at-least-node@npm:^1.0.0":
version: 1.0.0
resolution: "at-least-node@npm:1.0.0"
@@ -3009,6 +3053,15 @@ __metadata:
languageName: node
linkType: hard
"combined-stream@npm:^1.0.8":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
dependencies:
delayed-stream: "npm:~1.0.0"
checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5
languageName: node
linkType: hard
"commander@npm:^11.1.0":
version: 11.1.0
resolution: "commander@npm:11.1.0"
@@ -3450,6 +3503,13 @@ __metadata:
languageName: node
linkType: hard
"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19
languageName: node
linkType: hard
"delegates@npm:^1.0.0":
version: 1.0.0
resolution: "delegates@npm:1.0.0"
@@ -3738,6 +3798,18 @@ __metadata:
languageName: node
linkType: hard
"es-set-tostringtag@npm:^2.1.0":
version: 2.1.0
resolution: "es-set-tostringtag@npm:2.1.0"
dependencies:
es-errors: "npm:^1.3.0"
get-intrinsic: "npm:^1.2.6"
has-tostringtag: "npm:^1.0.2"
hasown: "npm:^2.0.2"
checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af
languageName: node
linkType: hard
"esbuild@npm:^0.20.2 || ^0.21.0 || ^0.22.0 || ^0.23.0":
version: 0.23.1
resolution: "esbuild@npm:0.23.1"
@@ -4090,6 +4162,36 @@ __metadata:
languageName: node
linkType: hard
"form-data-encoder@npm:1.7.2":
version: 1.7.2
resolution: "form-data-encoder@npm:1.7.2"
checksum: 10c0/56553768037b6d55d9de524f97fe70555f0e415e781cb56fc457a68263de3d40fadea2304d4beef2d40b1a851269bd7854e42c362107071892cb5238debe9464
languageName: node
linkType: hard
"form-data@npm:^4.0.4":
version: 4.0.4
resolution: "form-data@npm:4.0.4"
dependencies:
asynckit: "npm:^0.4.0"
combined-stream: "npm:^1.0.8"
es-set-tostringtag: "npm:^2.1.0"
hasown: "npm:^2.0.2"
mime-types: "npm:^2.1.12"
checksum: 10c0/373525a9a034b9d57073e55eab79e501a714ffac02e7a9b01be1c820780652b16e4101819785e1e18f8d98f0aee866cc654d660a435c378e16a72f2e7cac9695
languageName: node
linkType: hard
"formdata-node@npm:^4.3.2":
version: 4.4.1
resolution: "formdata-node@npm:4.4.1"
dependencies:
node-domexception: "npm:1.0.0"
web-streams-polyfill: "npm:4.0.0-beta.3"
checksum: 10c0/74151e7b228ffb33b565cec69182694ad07cc3fdd9126a8240468bb70a8ba66e97e097072b60bcb08729b24c7ce3fd3e0bd7f1f80df6f9f662b9656786e76f6a
languageName: node
linkType: hard
"fraction.js@npm:^4.3.7":
version: 4.3.7
resolution: "fraction.js@npm:4.3.7"
@@ -4186,7 +4288,7 @@ __metadata:
languageName: node
linkType: hard
"get-intrinsic@npm:^1.3.0":
"get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0":
version: 1.3.0
resolution: "get-intrinsic@npm:1.3.0"
dependencies:
@@ -4541,6 +4643,15 @@ __metadata:
languageName: node
linkType: hard
"humanize-ms@npm:^1.2.1":
version: 1.2.1
resolution: "humanize-ms@npm:1.2.1"
dependencies:
ms: "npm:^2.0.0"
checksum: 10c0/f34a2c20161d02303c2807badec2f3b49cbfbbb409abd4f95a07377ae01cfe6b59e3d15ac609cffcd8f2521f0eb37b7e1091acf65da99aa2a4f1ad63c21e7e7a
languageName: node
linkType: hard
"iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
@@ -5362,7 +5473,7 @@ __metadata:
languageName: node
linkType: hard
"mime-types@npm:^2.1.18, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
"mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
@@ -5550,7 +5661,7 @@ __metadata:
languageName: node
linkType: hard
"ms@npm:^2.1.1, ms@npm:^2.1.3":
"ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3":
version: 2.1.3
resolution: "ms@npm:2.1.3"
checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48
@@ -5716,6 +5827,13 @@ __metadata:
languageName: node
linkType: hard
"node-domexception@npm:1.0.0":
version: 1.0.0
resolution: "node-domexception@npm:1.0.0"
checksum: 10c0/5e5d63cda29856402df9472335af4bb13875e1927ad3be861dc5ebde38917aecbf9ae337923777af52a48c426b70148815e890a5d72760f1b4d758cc671b1a2b
languageName: node
linkType: hard
"node-fetch-native@npm:^1.6.4, node-fetch-native@npm:^1.6.6, node-fetch-native@npm:^1.6.7":
version: 1.6.7
resolution: "node-fetch-native@npm:1.6.7"
@@ -6056,9 +6174,17 @@ __metadata:
languageName: node
linkType: hard
"openai@npm:^5.20.2":
version: 5.20.2
resolution: "openai@npm:5.20.2"
"openai@npm:^4.66.0":
version: 4.104.0
resolution: "openai@npm:4.104.0"
dependencies:
"@types/node": "npm:^18.11.18"
"@types/node-fetch": "npm:^2.6.4"
abort-controller: "npm:^3.0.0"
agentkeepalive: "npm:^4.2.1"
form-data-encoder: "npm:1.7.2"
formdata-node: "npm:^4.3.2"
node-fetch: "npm:^2.6.7"
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
@@ -6069,7 +6195,7 @@ __metadata:
optional: true
bin:
openai: bin/cli
checksum: 10c0/9a8004d64feadfeba0cc15ccfdce6d1863be06dbaef7c1137aea606a4825657e77803cb106ccaf2913303ee1c1b55bc599fde08afb5158726e69011dfb38f6e3
checksum: 10c0/c4f2e837684ed96b8cec58c65a584646d667c69918f29052775e2e8c05ff5c860d8b58214a7770bc6895ca8602480420c1db6a5392dd250179eb0b91c2b19a2f
languageName: node
linkType: hard
@@ -6081,7 +6207,7 @@ __metadata:
dotenv: "npm:^17.2.2"
nuxt: "npm:^4.1.1"
nuxt-aos: "npm:1.2.5"
openai: "npm:^5.20.2"
openai: "npm:^4.66.0"
vue: "npm:^3.5.21"
vue-router: "npm:^4.5.1"
vuetify: "npm:3.9.0-beta.1"
@@ -8048,6 +8174,20 @@ __metadata:
languageName: node
linkType: hard
"undici-types@npm:~5.26.4":
version: 5.26.5
resolution: "undici-types@npm:5.26.5"
checksum: 10c0/bb673d7876c2d411b6eb6c560e0c571eef4a01c1c19925175d16e3a30c4c428181fb8d7ae802a261f283e4166a0ac435e2f505743aa9e45d893f9a3df017b501
languageName: node
linkType: hard
"undici-types@npm:~7.11.0":
version: 7.11.0
resolution: "undici-types@npm:7.11.0"
checksum: 10c0/24ff69baeaebc7d09f4dae68564df850b4ff561810148ff6c37d3fc64dd1460c55e59e604420495e73b1fb48594a1c98e69f6732086a24e01b88f2d926378af1
languageName: node
linkType: hard
"unenv@npm:^2.0.0-rc.21":
version: 2.0.0-rc.21
resolution: "unenv@npm:2.0.0-rc.21"
@@ -8645,6 +8785,13 @@ __metadata:
languageName: node
linkType: hard
"web-streams-polyfill@npm:4.0.0-beta.3":
version: 4.0.0-beta.3
resolution: "web-streams-polyfill@npm:4.0.0-beta.3"
checksum: 10c0/a9596779db2766990117ed3a158e0b0e9f69b887a6d6ba0779940259e95f99dc3922e534acc3e5a117b5f5905300f527d6fbf8a9f0957faf1d8e585ce3452e8e
languageName: node
linkType: hard
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"