From 69a7f180e4ef240cd420481e122184b5343cece7 Mon Sep 17 00:00:00 2001 From: Daniel Ringel <33063149+dringel@users.noreply.github.com> Date: Sat, 3 Jan 2026 17:13:15 +0100 Subject: [PATCH] Add files via upload --- app/api/chat/route.ts | 83 ++++++ app/api/chat/tools/search-vector-database.ts | 14 + app/api/chat/tools/web-search.ts | 32 +++ app/favicon.ico | Bin 0 -> 15406 bytes app/globals.css | 126 +++++++++ app/layout.tsx | 34 +++ app/page.tsx | 255 +++++++++++++++++++ app/parts/chat-header.tsx | 17 ++ app/terms/page.tsx | 207 +++++++++++++++ 9 files changed, 768 insertions(+) create mode 100644 app/api/chat/route.ts create mode 100644 app/api/chat/tools/search-vector-database.ts create mode 100644 app/api/chat/tools/web-search.ts create mode 100644 app/favicon.ico create mode 100644 app/globals.css create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/parts/chat-header.tsx create mode 100644 app/terms/page.tsx diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts new file mode 100644 index 0000000..18935cb --- /dev/null +++ b/app/api/chat/route.ts @@ -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, + }); +} \ No newline at end of file diff --git a/app/api/chat/tools/search-vector-database.ts b/app/api/chat/tools/search-vector-database.ts new file mode 100644 index 0000000..8b79a7c --- /dev/null +++ b/app/api/chat/tools/search-vector-database.ts @@ -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); + }, +}); + diff --git a/app/api/chat/tools/web-search.ts b/app/api/chat/tools/web-search.ts new file mode 100644 index 0000000..b1f6f51 --- /dev/null +++ b/app/api/chat/tools/web-search.ts @@ -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 []; + } + }, +}); \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..025975828ec978be9fb193a650a72b3f2bed14c1 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..5503b4c --- /dev/null +++ b/app/globals.css @@ -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; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..c82c675 --- /dev/null +++ b/app/layout.tsx @@ -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 ( + + + {children} + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..36e2a39 --- /dev/null +++ b/app/page.tsx @@ -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; +}; + +const loadMessagesFromStorage = (): { messages: UIMessage[]; durations: Record } => { + 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) => { + 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>({}); + const welcomeMessageShownRef = useRef(false); + + const stored = typeof window !== 'undefined' ? loadMessagesFromStorage() : { messages: [], durations: {} }; + const [initialMessages] = useState(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>({ + resolver: zodResolver(formSchema), + defaultValues: { + message: "", + }, + }); + + function onSubmit(data: z.infer) { + 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 ( +
+
+
+
+ + + + + + + Logo + + +

Chat with {AI_NAME}

+
+ + + +
+
+
+
+
+ {isClient ? ( + <> + + {status === "submitted" && ( +
+ +
+ )} + + ) : ( +
+ +
+ )} +
+
+
+
+
+
+
+ + ( + + + Message + +
+ { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + form.handleSubmit(onSubmit)(); + } + }} + /> + {(status == "ready" || status == "error") && ( + + )} + {(status == "streaming" || status == "submitted") && ( + + )} +
+
+ )} + /> +
+
+
+
+
+ © {new Date().getFullYear()} {OWNER_NAME} Terms of Use Powered by Ringel.AI +
+
+
+
+ ); +} diff --git a/app/parts/chat-header.tsx b/app/parts/chat-header.tsx new file mode 100644 index 0000000..2341211 --- /dev/null +++ b/app/parts/chat-header.tsx @@ -0,0 +1,17 @@ +import { cn } from "@/lib/utils"; + +export function ChatHeaderBlock({ children, className }: { children?: React.ReactNode, className?: string }) { + return ( +
+ {children} +
+ ) +} + +export function ChatHeader({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} \ No newline at end of file diff --git a/app/terms/page.tsx b/app/terms/page.tsx new file mode 100644 index 0000000..830d0ed --- /dev/null +++ b/app/terms/page.tsx @@ -0,0 +1,207 @@ +import { ArrowLeftIcon } from "lucide-react"; +import Link from "next/link"; +import { OWNER_NAME } from "@/config"; + +export default function Terms() { + return ( +
+
+ + + Back to Chatbot + +

MyAI3

+

Terms of Use / Disclaimer

+ +

+ 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. +

+ +
+

General Information

+
    +
  1. + Provider and Purpose: 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. +
  2. +
  3. + Third-Party Involvement:{" "} + 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. +
  4. +
  5. + No Guarantee of Accuracy:{" "} + 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. +
  6. +
+
+ +
+

Liability

+
    +
  1. + Use at Your Own Risk: The + AI Chatbot is provided on an "as-is" and "as-available" basis. To + the fullest extent permitted by law: +
      +
    • + {OWNER_NAME} disclaims all warranties, express or implied, + including but not limited to warranties of merchantability, + fitness for a particular purpose, and non-infringement. +
    • +
    • + {OWNER_NAME} is not liable for any errors, inaccuracies, or + omissions in the information provided by the AI Chatbot. +
    • +
    +
  2. +
  3. + + No Responsibility for Damages: + {" "} + 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. +
  4. +
  5. + + Modification or Discontinuation: + {" "} + I reserve the right to modify, suspend, or discontinue the AI + Chatbot's functionalities at any time without notice. +
  6. +
  7. + Future Fees: While the AI + Chatbot is currently provided free of charge, I reserve the right + to implement a fee for its use at any time. +
  8. +
+
+ +
+

User Responsibilities

+
    +
  1. + Eligibility: Use of the AI + Chatbot is restricted to individuals aged 18 or older. +
  2. +
  3. + Prohibited Conduct: By + using the AI Chatbot, you agree not to: +
      +
    • Post or transmit content that is defamatory, offensive, intimidating, illegal, racist, discriminatory, obscene, or otherwise inappropriate.
    • +
    • Use the AI Chatbot to engage in unlawful or unethical activities.
    • +
    • Attempt to compromise the security or functionality of the AI Chatbot
    • +
    • Copy, distribute, modify, reverse engineer, decompile, or extract the source code of the AI Chatbot without explicit written consent.
    • +
    +
  4. +
+
+ +
+

Data Privacy and Security

+
    +
  1. + No Privacy Guarantee: 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. +
  2. +
  3. + Public Information: Any + information you provide through the AI Chatbot is treated as + public. +
  4. +
  5. + Data Transmission: Inputs + may be transmitted to and processed by third-party services. +
  6. +
+
+ +
+

Ownership of Content and Commercial Use

+
    +
  1. + Surrender of Rights: 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. +
  2. +
  3. + Commercial and Research Use:{" "} + {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. +
  4. +
  5. + No Claim to Gains or Profits:{" "} + 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. +
  6. +
+
+ +
+

Indemnification

+

+ 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. +

+
+ +
+

Governing Law and Jurisdiction

+

+ 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. +

+
+ +
+

Acceptance of Terms

+

+ 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. +

+
+ +
+

Last Updated: November 17, 2025

+
+
+
+ ); +} \ No newline at end of file