Add files via upload

This commit is contained in:
Daniel Ringel 2026-01-03 17:13:15 +01:00 committed by GitHub
parent fbdfe9f11c
commit 69a7f180e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 768 additions and 0 deletions

83
app/api/chat/route.ts Normal file
View File

@ -0,0 +1,83 @@
import { streamText, UIMessage, convertToModelMessages, stepCountIs, createUIMessageStream, createUIMessageStreamResponse } from 'ai';
import { MODEL } from '@/config';
import { SYSTEM_PROMPT } from '@/prompts';
import { isContentFlagged } from '@/lib/moderation';
import { webSearch } from './tools/web-search';
import { vectorDatabaseSearch } from './tools/search-vector-database';
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const latestUserMessage = messages
.filter(msg => msg.role === 'user')
.pop();
if (latestUserMessage) {
const textParts = latestUserMessage.parts
.filter(part => part.type === 'text')
.map(part => 'text' in part ? part.text : '')
.join('');
if (textParts) {
const moderationResult = await isContentFlagged(textParts);
if (moderationResult.flagged) {
const stream = createUIMessageStream({
execute({ writer }) {
const textId = 'moderation-denial-text';
writer.write({
type: 'start',
});
writer.write({
type: 'text-start',
id: textId,
});
writer.write({
type: 'text-delta',
id: textId,
delta: moderationResult.denialMessage || "Your message violates our guidelines. I can't answer that.",
});
writer.write({
type: 'text-end',
id: textId,
});
writer.write({
type: 'finish',
});
},
});
return createUIMessageStreamResponse({ stream });
}
}
}
const result = streamText({
model: MODEL,
system: SYSTEM_PROMPT,
messages: convertToModelMessages(messages),
tools: {
webSearch,
vectorDatabaseSearch,
},
stopWhen: stepCountIs(10),
providerOptions: {
openai: {
reasoningSummary: 'auto',
reasoningEffort: 'low',
parallelToolCalls: false,
}
}
});
return result.toUIMessageStreamResponse({
sendReasoning: true,
});
}

View File

@ -0,0 +1,14 @@
import { tool } from "ai";
import { z } from "zod";
import { searchPinecone } from "@/lib/pinecone";
export const vectorDatabaseSearch = tool({
description: 'Search the vector database for information',
inputSchema: z.object({
query: z.string().describe('The query to search the vector database for. Optimally is a hypothetical answer for similarity search.'),
}),
execute: async ({ query }) => {
return await searchPinecone(query);
},
});

View File

@ -0,0 +1,32 @@
import { tool } from 'ai';
import { z } from 'zod';
import Exa from 'exa-js';
const exa = new Exa(process.env.EXA_API_KEY);
export const webSearch = tool({
description: 'Search the web for up-to-date information',
inputSchema: z.object({
query: z.string().min(1).describe('The search query'),
}),
execute: async ({ query }) => {
try {
const { results } = await exa.search(query, {
contents: {
text: true,
},
numResults: 3,
});
return results.map(result => ({
title: result.title,
url: result.url,
content: result.text?.slice(0, 1000) || '',
publishedDate: result.publishedDate,
}));
} catch (error) {
console.error('Error searching the web:', error);
return [];
}
},
});

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

126
app/globals.css Normal file
View File

@ -0,0 +1,126 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-inter);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
:root {
--radius: 0.625rem;
--background: oklch(0.995 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.1 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
.single-char-link {
@apply bg-card text-card-foreground px-2 py-1 rounded-full hover:underline hover:scale-105 transition-all duration-100 !text-foreground !no-underline border-input border;
}
}

34
app/layout.tsx Normal file
View File

@ -0,0 +1,34 @@
import type { Metadata } from "next";
import { Inter, Geist_Mono } from "next/font/google";
import "./globals.css";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "MyAI3",
description: "MyAI3",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${inter.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

255
app/page.tsx Normal file
View File

@ -0,0 +1,255 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { Controller, useForm } from "react-hook-form";
import { toast } from "sonner";
import * as z from "zod";
import { Button } from "@/components/ui/button";
import {
Field,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { useChat } from "@ai-sdk/react";
import { ArrowUp, Eraser, Loader2, Plus, PlusIcon, Square } from "lucide-react";
import { MessageWall } from "@/components/messages/message-wall";
import { ChatHeader } from "@/app/parts/chat-header";
import { ChatHeaderBlock } from "@/app/parts/chat-header";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { UIMessage } from "ai";
import { useEffect, useState, useRef } from "react";
import { AI_NAME, CLEAR_CHAT_TEXT, OWNER_NAME, WELCOME_MESSAGE } from "@/config";
import Image from "next/image";
import Link from "next/link";
const formSchema = z.object({
message: z
.string()
.min(1, "Message cannot be empty.")
.max(2000, "Message must be at most 2000 characters."),
});
const STORAGE_KEY = 'chat-messages';
type StorageData = {
messages: UIMessage[];
durations: Record<string, number>;
};
const loadMessagesFromStorage = (): { messages: UIMessage[]; durations: Record<string, number> } => {
if (typeof window === 'undefined') return { messages: [], durations: {} };
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) return { messages: [], durations: {} };
const parsed = JSON.parse(stored);
return {
messages: parsed.messages || [],
durations: parsed.durations || {},
};
} catch (error) {
console.error('Failed to load messages from localStorage:', error);
return { messages: [], durations: {} };
}
};
const saveMessagesToStorage = (messages: UIMessage[], durations: Record<string, number>) => {
if (typeof window === 'undefined') return;
try {
const data: StorageData = { messages, durations };
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (error) {
console.error('Failed to save messages to localStorage:', error);
}
};
export default function Chat() {
const [isClient, setIsClient] = useState(false);
const [durations, setDurations] = useState<Record<string, number>>({});
const welcomeMessageShownRef = useRef<boolean>(false);
const stored = typeof window !== 'undefined' ? loadMessagesFromStorage() : { messages: [], durations: {} };
const [initialMessages] = useState<UIMessage[]>(stored.messages);
const { messages, sendMessage, status, stop, setMessages } = useChat({
messages: initialMessages,
});
useEffect(() => {
setIsClient(true);
setDurations(stored.durations);
setMessages(stored.messages);
}, []);
useEffect(() => {
if (isClient) {
saveMessagesToStorage(messages, durations);
}
}, [durations, messages, isClient]);
const handleDurationChange = (key: string, duration: number) => {
setDurations((prevDurations) => {
const newDurations = { ...prevDurations };
newDurations[key] = duration;
return newDurations;
});
};
useEffect(() => {
if (isClient && initialMessages.length === 0 && !welcomeMessageShownRef.current) {
const welcomeMessage: UIMessage = {
id: `welcome-${Date.now()}`,
role: "assistant",
parts: [
{
type: "text",
text: WELCOME_MESSAGE,
},
],
};
setMessages([welcomeMessage]);
saveMessagesToStorage([welcomeMessage], {});
welcomeMessageShownRef.current = true;
}
}, [isClient, initialMessages.length, setMessages]);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
message: "",
},
});
function onSubmit(data: z.infer<typeof formSchema>) {
sendMessage({ text: data.message });
form.reset();
}
function clearChat() {
const newMessages: UIMessage[] = [];
const newDurations = {};
setMessages(newMessages);
setDurations(newDurations);
saveMessagesToStorage(newMessages, newDurations);
toast.success("Chat cleared");
}
return (
<div className="flex h-screen items-center justify-center font-sans dark:bg-black">
<main className="w-full dark:bg-black h-screen relative">
<div className="fixed top-0 left-0 right-0 z-50 bg-linear-to-b from-background via-background/50 to-transparent dark:bg-black overflow-visible pb-16">
<div className="relative overflow-visible">
<ChatHeader>
<ChatHeaderBlock />
<ChatHeaderBlock className="justify-center items-center">
<Avatar
className="size-8 ring-1 ring-primary"
>
<AvatarImage src="/logo.png" />
<AvatarFallback>
<Image src="/logo.png" alt="Logo" width={36} height={36} />
</AvatarFallback>
</Avatar>
<p className="tracking-tight">Chat with {AI_NAME}</p>
</ChatHeaderBlock>
<ChatHeaderBlock className="justify-end">
<Button
variant="outline"
size="sm"
className="cursor-pointer"
onClick={clearChat}
>
<Plus className="size-4" />
{CLEAR_CHAT_TEXT}
</Button>
</ChatHeaderBlock>
</ChatHeader>
</div>
</div>
<div className="h-screen overflow-y-auto px-5 py-4 w-full pt-[88px] pb-[150px]">
<div className="flex flex-col items-center justify-end min-h-full">
{isClient ? (
<>
<MessageWall messages={messages} status={status} durations={durations} onDurationChange={handleDurationChange} />
{status === "submitted" && (
<div className="flex justify-start max-w-3xl w-full">
<Loader2 className="size-4 animate-spin text-muted-foreground" />
</div>
)}
</>
) : (
<div className="flex justify-center max-w-2xl w-full">
<Loader2 className="size-4 animate-spin text-muted-foreground" />
</div>
)}
</div>
</div>
<div className="fixed bottom-0 left-0 right-0 z-50 bg-linear-to-t from-background via-background/50 to-transparent dark:bg-black overflow-visible pt-13">
<div className="w-full px-5 pt-5 pb-1 items-center flex justify-center relative overflow-visible">
<div className="message-fade-overlay" />
<div className="max-w-3xl w-full">
<form id="chat-form" onSubmit={form.handleSubmit(onSubmit)}>
<FieldGroup>
<Controller
name="message"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor="chat-form-message" className="sr-only">
Message
</FieldLabel>
<div className="relative h-13">
<Input
{...field}
id="chat-form-message"
className="h-15 pr-15 pl-5 bg-card rounded-[20px]"
placeholder="Type your message here..."
disabled={status === "streaming"}
aria-invalid={fieldState.invalid}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
form.handleSubmit(onSubmit)();
}
}}
/>
{(status == "ready" || status == "error") && (
<Button
className="absolute right-3 top-3 rounded-full"
type="submit"
disabled={!field.value.trim()}
size="icon"
>
<ArrowUp className="size-4" />
</Button>
)}
{(status == "streaming" || status == "submitted") && (
<Button
className="absolute right-2 top-2 rounded-full"
size="icon"
onClick={() => {
stop();
}}
>
<Square className="size-4" />
</Button>
)}
</div>
</Field>
)}
/>
</FieldGroup>
</form>
</div>
</div>
<div className="w-full px-5 py-3 items-center flex justify-center text-xs text-muted-foreground">
© {new Date().getFullYear()} {OWNER_NAME}&nbsp;<Link href="/terms" className="underline">Terms of Use</Link>&nbsp;Powered by&nbsp;<Link href="https://ringel.ai/" className="underline">Ringel.AI</Link>
</div>
</div>
</main>
</div >
);
}

17
app/parts/chat-header.tsx Normal file
View File

@ -0,0 +1,17 @@
import { cn } from "@/lib/utils";
export function ChatHeaderBlock({ children, className }: { children?: React.ReactNode, className?: string }) {
return (
<div className={cn("gap-2 flex flex-1", className)}>
{children}
</div>
)
}
export function ChatHeader({ children }: { children: React.ReactNode }) {
return (
<div className="w-full flex py-5 px-5 bg-linear-to-b from-background to-transparent">
{children}
</div>
)
}

207
app/terms/page.tsx Normal file
View File

@ -0,0 +1,207 @@
import { ArrowLeftIcon } from "lucide-react";
import Link from "next/link";
import { OWNER_NAME } from "@/config";
export default function Terms() {
return (
<div className="w-full flex justify-center p-10">
<div className="w-full max-w-screen-md space-y-6">
<Link
href="/"
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 underline"
>
<ArrowLeftIcon className="w-4 h-4" />
Back to Chatbot
</Link>
<h1 className="text-3xl font-bold">MyAI3</h1>
<h2 className="text-2xl font-semibold">Terms of Use / Disclaimer</h2>
<p className="text-gray-700">
The following terms of use govern access to and use of the MyAI3
Assistant ("AI Chatbot"), an artificial intelligence tool provided by
{OWNER_NAME} ("I", "me", or "myself"). By engaging with the AI
Chatbot, you agree to these terms. If you do not agree, you may not
use the AI Chatbot.
</p>
<div className="space-y-4">
<h3 className="text-xl font-semibold">General Information</h3>
<ol className="list-decimal list-inside space-y-3">
<li className="text-gray-700">
<span className="font-semibold">Provider and Purpose:</span> The
AI Chatbot is a tool developed and maintained by {OWNER_NAME}. It
is intended solely to assist users with questions and coursework
related to courses taught by {OWNER_NAME}. The AI Chatbot is not
affiliated with, endorsed by, or operated by the course provider.
</li>
<li className="text-gray-700">
<span className="font-semibold">Third-Party Involvement:</span>{" "}
The AI Chatbot utilizes multiple third-party platforms and
vendors, some of which operate outside the United States. Your
inputs may be transmitted, processed, and stored by these
third-party systems. As such, confidentiality, security, and privacy
cannot be guaranteed, and data transmission may be inherently
insecure and subject to interception.
</li>
<li className="text-gray-700">
<span className="font-semibold">No Guarantee of Accuracy:</span>{" "}
The AI Chatbot is designed to provide helpful and relevant
responses but may deliver inaccurate, incomplete, or outdated
information. Users are strongly encouraged to independently verify
any information before relying on it for decisions or actions.
</li>
</ol>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Liability</h3>
<ol className="list-decimal list-inside space-y-3">
<li className="text-gray-700">
<span className="font-semibold">Use at Your Own Risk:</span> The
AI Chatbot is provided on an "as-is" and "as-available" basis. To
the fullest extent permitted by law:
<ul className="list-disc list-inside ml-6 mt-2 space-y-2">
<li>
{OWNER_NAME} disclaims all warranties, express or implied,
including but not limited to warranties of merchantability,
fitness for a particular purpose, and non-infringement.
</li>
<li>
{OWNER_NAME} is not liable for any errors, inaccuracies, or
omissions in the information provided by the AI Chatbot.
</li>
</ul>
</li>
<li className="text-gray-700">
<span className="font-semibold">
No Responsibility for Damages:
</span>{" "}
Under no circumstances shall {OWNER_NAME}, his collaborators,
partners, affiliated entities, or representatives be liable for
any direct, indirect, incidental, consequential, special, or
punitive damages arising out of or in connection with the use of
the AI Chatbot.
</li>
<li className="text-gray-700">
<span className="font-semibold">
Modification or Discontinuation:
</span>{" "}
I reserve the right to modify, suspend, or discontinue the AI
Chatbot's functionalities at any time without notice.
</li>
<li className="text-gray-700">
<span className="font-semibold">Future Fees:</span> While the AI
Chatbot is currently provided free of charge, I reserve the right
to implement a fee for its use at any time.
</li>
</ol>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">User Responsibilities</h3>
<ol className="list-decimal list-inside space-y-3">
<li className="text-gray-700">
<span className="font-semibold">Eligibility:</span> Use of the AI
Chatbot is restricted to individuals aged 18 or older.
</li>
<li className="text-gray-700">
<span className="font-semibold">Prohibited Conduct:</span> By
using the AI Chatbot, you agree not to:
<ul className="list-disc list-inside ml-6 mt-2 space-y-2">
<li>Post or transmit content that is defamatory, offensive, intimidating, illegal, racist, discriminatory, obscene, or otherwise inappropriate.</li>
<li>Use the AI Chatbot to engage in unlawful or unethical activities.</li>
<li>Attempt to compromise the security or functionality of the AI Chatbot</li>
<li>Copy, distribute, modify, reverse engineer, decompile, or extract the source code of the AI Chatbot without explicit written consent.</li>
</ul>
</li>
</ol>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Data Privacy and Security</h3>
<ol className="list-decimal list-inside space-y-3">
<li className="text-gray-700">
<span className="font-semibold">No Privacy Guarantee:</span> The
AI Chatbot does not guarantee privacy, confidentiality, or
security of the information you provide. Conversations may be
reviewed by {OWNER_NAME}, collaborators, partners, or affiliated
entities for purposes such as improving the AI Chatbot, developing
course materials, and conducting research.
</li>
<li className="text-gray-700">
<span className="font-semibold">Public Information:</span> Any
information you provide through the AI Chatbot is treated as
public.
</li>
<li className="text-gray-700">
<span className="font-semibold">Data Transmission:</span> Inputs
may be transmitted to and processed by third-party services.
</li>
</ol>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Ownership of Content and Commercial Use</h3>
<ol className="list-decimal list-inside space-y-3">
<li className="text-gray-700">
<span className="font-semibold">Surrender of Rights:</span> By
using the AI Chatbot, you irrevocably assign and surrender all rights,
title, interest, and intellectual property rights in any content, inputs
you provide, and outputs generated by the AI Chatbot to {OWNER_NAME}.
This includes, but is not limited to, text, questions, and conversations.
</li>
<li className="text-gray-700">
<span className="font-semibold">Commercial and Research Use:</span>{" "}
{OWNER_NAME} reserves the right to use any input provided by users and
any output generated by the AI Chatbot for commercial purposes, research,
or other activities without compensation or notification to users.
</li>
<li className="text-gray-700">
<span className="font-semibold">No Claim to Gains or Profits:</span>{" "}
Users agree that they have no rights, claims, or entitlement to
any gains, profits, or benefits derived from the use or
exploitation of the content provided to the AI Chatbot.
</li>
</ol>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Indemnification</h3>
<p className="text-gray-700">
By using the AI Chatbot, you agree to indemnify and hold harmless
{OWNER_NAME}, his collaborators, partners, affiliated entities, and
representatives from any claims, damages, losses, or liabilities
arising out of your use of the AI Chatbot or violation of these
terms.
</p>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Governing Law and Jurisdiction</h3>
<p className="text-gray-700">
These terms are governed by the laws of the State of North Carolina,
United States. Additional jurisdictions may apply for users outside
the United States, subject to applicable local laws. In case of
conflicts, the laws of North Carolina shall prevail to the extent
permissible. Any disputes arising under or in connection with these
terms shall be subject to the exclusive jurisdiction of the courts
located in North Carolina.
</p>
</div>
<div className="space-y-4">
<h3 className="text-xl font-semibold">Acceptance of Terms</h3>
<p className="text-gray-700">
By using the AI Chatbot, you confirm that you have read, understood,
and agreed to these Terms of Use and Disclaimer. If you do not
agree with any part of these terms, you may not use the AI Chatbot.
</p>
</div>
<div className="mt-8 text-sm text-gray-600">
<p>Last Updated: November 17, 2025</p>
</div>
</div>
</div>
);
}