Chat with {AI_NAME}
+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 0000000..0259758 Binary files /dev/null and b/app/favicon.ico differ 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: RecordChat with {AI_NAME}
++ 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. +
+ ++ 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. +
++ 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. +
++ 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
+