diff --git a/.claude/settings.json b/.claude/settings.json index 83d4fa6..3398d5f 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,3 +1,4 @@ +{ "permissions": { "defaultMode": "plan", "deny": [ @@ -13,4 +14,5 @@ "Bash(git stash:*)", "Read(~/.claude/projects/**)" ] - }, \ No newline at end of file + } +} diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..51e0f2c --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,9 @@ +{ + "features": { + "ghcr.io/anthropics/devcontainer-features/claude-code:1": { + "version": "1.0.5", + "resolved": "ghcr.io/anthropics/devcontainer-features/claude-code@sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a", + "integrity": "sha256:cfc2e7d3e9fd3b9b01f8d5cb158508a884c8c0ede2e23ed10f32dea5d4ffe69a" + } + } +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 331e834..d010687 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,13 +7,18 @@ "mounts": [ "source=claude-config-${devcontainerId},target=/home/node/.claude,type=volume", "source=${localEnv:HOME}/.ssh/config,target=/home/node/.ssh/config,type=bind,readonly", - "source=${localEnv:HOME}/.ssh/known_hosts,target=/home/node/.ssh/known_hosts,type=bind,readonly" + "source=${localEnv:HOME}/.ssh/known_hosts,target=/home/node/.ssh/known_hosts,type=bind,readonly", + "source=npm-cache,target=/home/node/.npm,type=volume" ], "forwardPorts": [3000], + "containerEnv": { + "NEXT_TELEMETRY_DISABLED": "1", + "NPM_CONFIG_UPDATE_NOTIFIER": "false" + }, "portsAttributes": { "3000": { "label": "App", "onAutoForward": "openBrowser" } }, - "postCreateCommand": "sudo chown -R node:node /home/node/.claude && cd frontend-starter && npm install", + "postCreateCommand": "sudo chown -R node:node /home/node/.claude /home/node/.npm && cd frontend-starter && npm install", "postStartCommand": "cd frontend-starter && npm install --no-audit --no-fund", "postAttachCommand": "cd frontend-starter && npx -y kill-port 3000 && npm run dev", "customizations": { @@ -33,4 +38,5 @@ "bradlc.vscode-tailwindcss" ] } - } \ No newline at end of file + } +} diff --git a/.gitignore b/.gitignore index dcc81d9..a71711b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules/ .env.* !.env.local.example data/ -.claude/settings.local.json \ No newline at end of file +.claude/settings.local.json +next-env.d.ts diff --git a/frontend-starter/app/api/chat/route.ts b/frontend-starter/app/api/chat/route.ts deleted file mode 100644 index 384c464..0000000 --- a/frontend-starter/app/api/chat/route.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { streamText, convertToModelMessages, type UIMessage } from "ai"; -import { getModel } from "@/lib/ai/model"; - -// Streaming-Chat-Endpoint (Muster C). Wird von useChat (Default /api/chat) aufgerufen. -// -// ACHTUNG: Dieser Endpoint hat KEINEN Login. Wer die Adresse der App kennt, kann hier -// auf Kosten des hinterlegten ANTHROPIC_API_KEY chatten (offener KI-Proxy). -// Deshalb ist er standardmäßig AUS und läuft nur mit CHAT_ENABLED=true. -// Nie mit echtem Key öffentlich veröffentlichen, solange die App keinen Schutz hat -// (Login, Zugangscode). Die Grenzen unten dämpfen Missbrauch nur, sie verhindern ihn nicht. - -const MAX_MESSAGES = 40; -const MAX_REQUEST_CHARS = 60000; -const MAX_OUTPUT_TOKENS = 1024; - -function parseJson(text: string): unknown { - try { - return JSON.parse(text); - } catch { - return null; - } -} - -export async function POST(req: Request) { - if (process.env.CHAT_ENABLED !== "true") { - return new Response( - "KI-Chat ist deaktiviert. Zum Einschalten CHAT_ENABLED=true setzen (nur mit Schutz öffentlich betreiben).", - { status: 403 }, - ); - } - - const raw = await req.text(); - if (raw.length > MAX_REQUEST_CHARS) { - return new Response("Anfrage zu groß.", { status: 413 }); - } - - const body = parseJson(raw) as { messages?: unknown } | null; - const messages = body?.messages; - if (!Array.isArray(messages) || messages.length === 0) { - return new Response("Ungültige Anfrage.", { status: 400 }); - } - if (messages.length > MAX_MESSAGES) { - return new Response("Der Chat ist zu lang. Bitte die Seite neu laden.", { status: 413 }); - } - - const result = streamText({ - model: getModel(), - messages: await convertToModelMessages(messages as UIMessage[]), - maxOutputTokens: MAX_OUTPUT_TOKENS, - }); - return result.toUIMessageStreamResponse(); -} diff --git a/frontend-starter/app/chat/page.tsx b/frontend-starter/app/chat/page.tsx deleted file mode 100644 index 6299427..0000000 --- a/frontend-starter/app/chat/page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useChat } from "@ai-sdk/react"; - -// Muster C: KI-Chat mit Token-Streaming. Ruft POST /api/chat auf. -// Achtung: /api/chat hat keinen Login und ist nur mit CHAT_ENABLED=true aktiv (siehe route.ts). -// In der Schulung bleibt der Chat aus. KI läuft über einen n8n-Workflow. -export default function ChatPage() { - const { messages, sendMessage, status, error } = useChat(); - const [input, setInput] = useState(""); - const busy = status === "submitted" || status === "streaming"; - - function onSubmit(e: React.FormEvent) { - e.preventDefault(); - if (!input.trim() || busy) return; - sendMessage({ text: input }); - setInput(""); - } - - return ( -
-

KI-Chat (Muster C)

-

- Streamt Antworten von Claude. Braucht ANTHROPIC_API_KEY und CHAT_ENABLED=true. Ohne - Login nie mit echtem Key öffentlich veröffentlichen. -

- -
- {messages.map((message) => ( -
- {message.parts.map((part, i) => (part.type === "text" ? {part.text} : null))} -
- ))} -
- - {error &&
{error.message}
} - -
- setInput(e.target.value)} - placeholder="Frag mich etwas …" - className="ep-feld flex-1" - /> - -
-
- ); -} diff --git a/frontend-starter/app/layout.tsx b/frontend-starter/app/layout.tsx index 936e9ce..2aadb52 100644 --- a/frontend-starter/app/layout.tsx +++ b/frontend-starter/app/layout.tsx @@ -4,12 +4,8 @@ import "./globals.css"; import { EpKopf } from "@/components/EpKopf"; import { EpFuss } from "@/components/EpFuss"; import { APP_BESCHREIBUNG, APP_TITEL } from "@/lib/app"; -import { Inter } from "next/font/google"; import { cn } from "@/lib/utils"; -const inter = Inter({subsets:['latin'],variable:'--font-sans'}); - - // Hausschrift von ep-reisen.de (Lato, freie Lizenz OFL). Die Dateien liegen im Projekt, // damit der Build ohne Internet klappt. Lato hat keine Stärke 600, font-semibold erscheint fett. const lato = localFont({ @@ -37,7 +33,7 @@ export const viewport: Viewport = { // E&P-Layout für jede Seite: Kopf mit Logo, Inhalt, Fuß. Seiten liefern nur ihren Inhalt, kein eigenes
. export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + Zum Inhalt springen diff --git a/frontend-starter/app/page.tsx b/frontend-starter/app/page.tsx index 02fa49c..c5819f6 100644 --- a/frontend-starter/app/page.tsx +++ b/frontend-starter/app/page.tsx @@ -1,76 +1,17 @@ -"use client"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { APP_BESCHREIBUNG, APP_TITEL } from "@/lib/app"; -import { useState } from "react"; -import { postWebhook } from "@/lib/n8n"; - -type HelloResponse = { message?: string }; - -// Pfad des eigenen Webhooks mit Kürzel, zum Beispiel "anna-hello" (ohne /webhook/). -// In der E&P-n8n gehört jeder Pfad genau einem Workflow. Ohne Angabe ruft die Seite nichts auf. -const HELLO_PATH = (process.env.NEXT_PUBLIC_HELLO_PATH || "").trim(); - -// Beispielseite im E&P-Layout. Kopf und Fuß kommen aus app/layout.tsx, -// Farben und Bausteine (ep-karte, ep-feld, ep-knopf, ep-cta, ep-hinweis, ep-fehler) aus app/globals.css. +// Startseite im E&P-Layout. Kopf und Fuß kommen aus app/layout.tsx, +// Farben und Bausteine (ep-karte, ep-ueberschrift, …) aus app/globals.css. export default function Home() { - const [name, setName] = useState(""); - const [answer, setAnswer] = useState(null); - const [error, setError] = useState(null); - const [loading, setLoading] = useState(false); - - async function onSubmit(e: React.FormEvent) { - e.preventDefault(); - setError(null); - setAnswer(null); - if (!HELLO_PATH) { - setError( - "NEXT_PUBLIC_HELLO_PATH fehlt. Beim ersten Veröffentlichen den eigenen Pfad mitgeben, zum Beispiel anna-hello.", - ); - return; - } - setLoading(true); - try { - const res = await postWebhook(HELLO_PATH, { name }); - setAnswer(res.message ?? JSON.stringify(res)); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } - } - return ( -
-
-

Hallo aus n8n

-

- Das Formular ruft einen n8n-Webhook deines eigenen Workflows auf (Vorlage hello-webhook.json, - Pfad /webhook/{HELLO_PATH || "anna-hello"}). -

-
- -
- - setName(e.target.value)} - placeholder="Max" - className="ep-feld" - required - /> - -
- - {answer &&
{answer}
} - {error && ( -
- {error} -
- )} -
+ + +

{APP_TITEL}

+
+ +

{APP_BESCHREIBUNG}

+
+
); } diff --git a/frontend-starter/components/EpFuss.tsx b/frontend-starter/components/EpFuss.tsx index 4816212..fa59df8 100644 --- a/frontend-starter/components/EpFuss.tsx +++ b/frontend-starter/components/EpFuss.tsx @@ -3,7 +3,7 @@ export function EpFuss() { return (
- E&P Reisen & Events · entstanden in der KI-Schulung + E&P Reisen & Events · interne Anwendung
); diff --git a/frontend-starter/components/ui/button.tsx b/frontend-starter/components/ui/button.tsx index 4f39809..2e4ed65 100644 --- a/frontend-starter/components/ui/button.tsx +++ b/frontend-starter/components/ui/button.tsx @@ -1,6 +1,6 @@ import { Button as ButtonPrimitive } from "@base-ui/react/button" import { cva, type VariantProps } from "class-variance-authority" -import { cn } from "cn" +import { cn } from "@/lib/utils" const buttonVariants = cva( "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", diff --git a/frontend-starter/components/ui/card.tsx b/frontend-starter/components/ui/card.tsx new file mode 100644 index 0000000..b14d112 --- /dev/null +++ b/frontend-starter/components/ui/card.tsx @@ -0,0 +1,99 @@ +import * as React from "react" +import { cn } from "@/lib/utils" + +function Card({ + className, + size = "default", + ...props +}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { + return ( +
img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] dark:ring-foreground/10 *:[img:first-child]:rounded-t-4xl *:[img:last-child]:rounded-b-4xl", + className + )} + {...props} + /> + ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/frontend-starter/lib/ai/model.ts b/frontend-starter/lib/ai/model.ts deleted file mode 100644 index 7427a2d..0000000 --- a/frontend-starter/lib/ai/model.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Muster C: Provider und Modell an einer Stelle. -// Der Standard-Provider liest ANTHROPIC_API_KEY aus der Umgebung. -import { anthropic } from "@ai-sdk/anthropic"; - -// Ein günstiges Modell, weil jeder Aufruf Geld kostet (Claude Sonnet 5: 2 $ Input / 10 $ Output pro 1 Mio. Tokens). -export const getModel = () => anthropic("claude-sonnet-5"); diff --git a/frontend-starter/lib/n8n.ts b/frontend-starter/lib/n8n.ts deleted file mode 100644 index edf72c5..0000000 --- a/frontend-starter/lib/n8n.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Muster A: n8n-Webhook aus dem Frontend aufrufen. -// Die Basis-Adresse wird beim Bauen eingesetzt (NEXT_PUBLIC_*). Ohne Angabe gilt die E&P-n8n. - -const BASE = (process.env.NEXT_PUBLIC_N8N_BASE || "https://n8n.ep-reisen.app").replace(/\/+$/, ""); - -export function webhookUrl(path: string): string { - const clean = path.replace(/^\/+/, ""); - return `${BASE}/webhook/${clean}`; -} - -export async function postWebhook( - path: string, - body: unknown, -): Promise { - const res = await fetch(webhookUrl(path), { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - if (!res.ok) { - throw new Error(`Webhook-Fehler ${res.status}: ${await res.text()}`); - } - // n8n kann JSON oder Text zurückgeben: - const text = await res.text(); - try { - return JSON.parse(text) as T; - } catch { - return text as T; - } -} diff --git a/frontend-starter/lib/utils.ts b/frontend-starter/lib/utils.ts index f815ded..bd0c391 100644 --- a/frontend-starter/lib/utils.ts +++ b/frontend-starter/lib/utils.ts @@ -1 +1,6 @@ -export { cn } from "cn" +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/frontend-starter/next-env.d.ts b/frontend-starter/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/frontend-starter/next-env.d.ts +++ b/frontend-starter/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/frontend-starter/package-lock.json b/frontend-starter/package-lock.json index b13ff4d..9dade2f 100644 --- a/frontend-starter/package-lock.json +++ b/frontend-starter/package-lock.json @@ -8,17 +8,15 @@ "name": "frontend-starter", "version": "0.1.0", "dependencies": { - "@ai-sdk/anthropic": "^3.0.0", - "@ai-sdk/react": "^3.0.0", "@base-ui/react": "^1.9.0", - "ai": "^6.0.0", "class-variance-authority": "^0.7.1", - "cn": "^0.5.0", + "clsx": "^2.1.1", "lucide-react": "^1.63.0", "next": "^16.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", "shadcn": "^4.23.0", + "tailwind-merge": "^3.7.0", "tw-animate-css": "^1.4.0" }, "devDependencies": { @@ -30,86 +28,6 @@ "typescript": "^5.9.0" } }, - "node_modules/@ai-sdk/anthropic": { - "version": "3.0.81", - "resolved": "https://registry.npmjs.org/@ai-sdk/anthropic/-/anthropic-3.0.81.tgz", - "integrity": "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.10", - "@ai-sdk/provider-utils": "4.0.27" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/gateway": { - "version": "3.0.124", - "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-3.0.124.tgz", - "integrity": "sha512-h8CrmbSG+8X0C+M/E1M4oiDHYevqwbzAPN+uLRHS0eJaatF2MZ+juNtOHXNOjk7Bsk9mD2RjYMjJO9dFkb9I7Q==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.10", - "@ai-sdk/provider-utils": "4.0.27", - "@vercel/oidc": "3.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/provider": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.10.tgz", - "integrity": "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==", - "license": "Apache-2.0", - "dependencies": { - "json-schema": "^0.4.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@ai-sdk/provider-utils": { - "version": "4.0.27", - "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-4.0.27.tgz", - "integrity": "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider": "3.0.10", - "@standard-schema/spec": "^1.1.0", - "eventsource-parser": "^3.0.8" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, - "node_modules/@ai-sdk/react": { - "version": "3.0.198", - "resolved": "https://registry.npmjs.org/@ai-sdk/react/-/react-3.0.198.tgz", - "integrity": "sha512-ozlxMidzvKXAefvnq95Y34rJ5MipXABIv1bg2RLEnWUBxGrKxNHrYl0fTfi6grTN88wSXMZYaMY2oHMCHDFuJw==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/provider-utils": "4.0.27", - "ai": "6.0.196", - "swr": "^2.2.5", - "throttleit": "2.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1" - } - }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -1503,6 +1421,8 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=8.0.0" } @@ -1567,12 +1487,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1898,33 +1812,6 @@ "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", "integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==" }, - "node_modules/@vercel/oidc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.2.0.tgz", - "integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==", - "license": "Apache-2.0", - "engines": { - "node": ">= 20" - } - }, - "node_modules/ai": { - "version": "6.0.196", - "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.196.tgz", - "integrity": "sha512-2T45UeqKL4a11KQ14I5i1YYHOvCFrMF478E1k6PVjlQSGUvXSv4xrxIaQbUL4qgv91DADSbddwv3oR49pPAK3g==", - "license": "Apache-2.0", - "dependencies": { - "@ai-sdk/gateway": "3.0.124", - "@ai-sdk/provider": "3.0.10", - "@ai-sdk/provider-utils": "4.0.27", - "@opentelemetry/api": "^1.9.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "zod": "^3.25.76 || ^4.1.8" - } - }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -2154,21 +2041,11 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/cn": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cn/-/cn-0.5.0.tgz", - "integrity": "sha512-JGEixZEpfFXm1P5uXwvEobOFLlHwIWi5v4tjd1ZwfiYmcEilkuD2l+054rvJcYCR1B+uVThHeUbgmNz1FwuxuA==", - "bin": { - "cn": "bin/cn.mjs" - }, - "engines": { - "node": ">=22" - } - }, "node_modules/code-block-writer": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", @@ -2348,15 +2225,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2464,15 +2332,6 @@ "node": ">=4" } }, - "node_modules/eventsource-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", - "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/execa": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", @@ -2877,12 +2736,6 @@ "node": ">=6" } }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "license": "(AFL-2.1 OR BSD-3-Clause)" - }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -4204,19 +4057,6 @@ } } }, - "node_modules/swr": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", - "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.3", - "use-sync-external-store": "^1.6.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/systeminformation": { "version": "5.33.18", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.18.tgz", @@ -4242,6 +4082,16 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, + "node_modules/tailwind-merge": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.7.0.tgz", + "integrity": "sha512-XPPUyAc+cvspz3lHTcR/QgPfW2A0lv/xQNIjX3HGhLR+Nq2lHaLq5MtTesHn8GUr3W3DguT2KT5x3NVgRtYwmA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", @@ -4263,18 +4113,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/throttleit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-2.1.0.tgz", - "integrity": "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", diff --git a/frontend-starter/package.json b/frontend-starter/package.json index 27ad6ac..3bd47ac 100644 --- a/frontend-starter/package.json +++ b/frontend-starter/package.json @@ -8,17 +8,15 @@ "start": "next start -p 3000" }, "dependencies": { - "@ai-sdk/anthropic": "^3.0.0", - "@ai-sdk/react": "^3.0.0", "@base-ui/react": "^1.9.0", - "ai": "^6.0.0", "class-variance-authority": "^0.7.1", - "cn": "^0.5.0", + "clsx": "^2.1.1", "lucide-react": "^1.63.0", "next": "^16.2.0", "react": "^19.2.0", "react-dom": "^19.2.0", "shadcn": "^4.23.0", + "tailwind-merge": "^3.7.0", "tw-animate-css": "^1.4.0" }, "devDependencies": { @@ -29,4 +27,4 @@ "tailwindcss": "^4.3.0", "typescript": "^5.9.0" } -} \ No newline at end of file +}