7 — Code-Skelett als Fallback
Falls Live-/speckit.implement ausfällt, diese drei Dateien direkt
zeigen / committen:
app/api/chat/route.ts
import Anthropic from "@anthropic-ai/sdk";
export const runtime = "nodejs";
export async function POST(req: Request) {
const { prompt } = (await req.json()) as { prompt: string };
const client = new Anthropic();
const stream = client.messages.stream({
model: "claude-haiku-4-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
});
const encoder = new TextEncoder();
const body = new ReadableStream<Uint8Array>({
async start(controller) {
try {
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
controller.enqueue(encoder.encode(event.delta.text));
}
}
controller.close();
} catch (err) {
controller.error(err);
}
},
cancel() {
stream.controller.abort();
},
});
return new Response(body, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}
app/components/chat.tsx
"use client";
import { useRef, useState } from "react";
export function Chat() {
const [prompt, setPrompt] = useState("");
const [answer, setAnswer] = useState("");
const [busy, setBusy] = useState(false);
const aborter = useRef<AbortController | null>(null);
async function ask() {
setBusy(true);
setAnswer("");
aborter.current = new AbortController();
try {
const res = await fetch("/api/chat", {
method: "POST",
body: JSON.stringify({ prompt }),
signal: aborter.current.signal,
});
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
setAnswer((a) => a + decoder.decode(value, { stream: true }));
}
} catch (err) {
if ((err as Error).name !== "AbortError") {
setAnswer((a) => a + `\n\n[Fehler: ${(err as Error).message}]`);
}
} finally {
setBusy(false);
}
}
return (
<main className="mx-auto max-w-2xl space-y-4 p-6">
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
rows={3}
className="w-full rounded border p-2"
placeholder="Frag Claude etwas…"
/>
<div className="flex gap-2">
<button
onClick={ask}
disabled={busy || !prompt.trim()}
className="rounded bg-black px-4 py-2 text-white disabled:opacity-50"
>
{busy ? "Generating…" : "Ask"}
</button>
<button
onClick={() => aborter.current?.abort()}
disabled={!busy}
className="rounded border px-4 py-2 disabled:opacity-50"
>
Stop
</button>
</div>
<pre className="whitespace-pre-wrap rounded border bg-gray-50 p-4">
{answer}
</pre>
</main>
);
}
app/page.tsx
import { Chat } from "./components/chat";
export default function Page() {
return <Chat />;
}