Upload folder using huggingface_hub
Browse files- .gitignore +41 -0
- README.md +51 -10
- eslint.config.mjs +16 -0
- next.config.ts +7 -0
- package-lock.json +0 -0
- package.json +28 -0
- postcss.config.mjs +5 -0
- public/cat.png +0 -0
- public/favicon.ico +1 -0
- public/file.svg +1 -0
- public/globe.svg +1 -0
- public/next.svg +1 -0
- public/vercel.svg +1 -0
- public/window.svg +1 -0
- src/app/globals.css +53 -0
- src/app/layout.tsx +40 -0
- src/app/page.tsx +13 -0
- src/components/Chat.tsx +135 -0
- src/components/ChatInput.tsx +76 -0
- src/components/ChatMessage.tsx +42 -0
- src/components/Header.tsx +25 -0
- tsconfig.json +27 -0
.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
README.md
CHANGED
|
@@ -1,10 +1,51 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CatGPT
|
| 2 |
+
|
| 3 |
+
A fun ChatGPT parody where the AI is replaced by a cat that only responds with "meow". This project is built with Next.js and styled with Tailwind CSS.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- ChatGPT-like interface
|
| 8 |
+
- Cat-themed design
|
| 9 |
+
- The AI assistant only responds with "meow"
|
| 10 |
+
- Responsive design for all devices
|
| 11 |
+
|
| 12 |
+
## Getting Started
|
| 13 |
+
|
| 14 |
+
### Prerequisites
|
| 15 |
+
|
| 16 |
+
- Node.js 18.0.0 or later
|
| 17 |
+
|
| 18 |
+
### Installation
|
| 19 |
+
|
| 20 |
+
1. Clone the repository:
|
| 21 |
+
```bash
|
| 22 |
+
git clone https://github.com/yourusername/catgpt.git
|
| 23 |
+
cd catgpt
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
2. Install dependencies:
|
| 27 |
+
```bash
|
| 28 |
+
npm install
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
3. Run the development server:
|
| 32 |
+
```bash
|
| 33 |
+
npm run dev
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
4. Open [http://localhost:3000](http://localhost:3000) in your browser to see the result.
|
| 37 |
+
|
| 38 |
+
## Tech Stack
|
| 39 |
+
|
| 40 |
+
- [Next.js](https://nextjs.org/) - React framework
|
| 41 |
+
- [Tailwind CSS](https://tailwindcss.com/) - CSS framework
|
| 42 |
+
- [TypeScript](https://www.typescriptlang.org/) - Type checking
|
| 43 |
+
|
| 44 |
+
## License
|
| 45 |
+
|
| 46 |
+
This project is open source and available under the [MIT License](LICENSE).
|
| 47 |
+
|
| 48 |
+
## Acknowledgements
|
| 49 |
+
|
| 50 |
+
- Inspired by ChatGPT
|
| 51 |
+
- Built for fun and educational purposes
|
eslint.config.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { dirname } from "path";
|
| 2 |
+
import { fileURLToPath } from "url";
|
| 3 |
+
import { FlatCompat } from "@eslint/eslintrc";
|
| 4 |
+
|
| 5 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 6 |
+
const __dirname = dirname(__filename);
|
| 7 |
+
|
| 8 |
+
const compat = new FlatCompat({
|
| 9 |
+
baseDirectory: __dirname,
|
| 10 |
+
});
|
| 11 |
+
|
| 12 |
+
const eslintConfig = [
|
| 13 |
+
...compat.extends("next/core-web-vitals", "next/typescript"),
|
| 14 |
+
];
|
| 15 |
+
|
| 16 |
+
export default eslintConfig;
|
next.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
/* config options here */
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default nextConfig;
|
package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "cat.opencharacter.org",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev --turbopack",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "next lint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"@next/third-parties": "^15.2.2",
|
| 13 |
+
"next": "15.2.2",
|
| 14 |
+
"react": "^19.0.0",
|
| 15 |
+
"react-dom": "^19.0.0"
|
| 16 |
+
},
|
| 17 |
+
"devDependencies": {
|
| 18 |
+
"@eslint/eslintrc": "^3",
|
| 19 |
+
"@tailwindcss/postcss": "^4",
|
| 20 |
+
"@types/node": "^20",
|
| 21 |
+
"@types/react": "^19",
|
| 22 |
+
"@types/react-dom": "^19",
|
| 23 |
+
"eslint": "^9",
|
| 24 |
+
"eslint-config-next": "15.2.2",
|
| 25 |
+
"tailwindcss": "^4",
|
| 26 |
+
"typescript": "^5"
|
| 27 |
+
}
|
| 28 |
+
}
|
postcss.config.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: ["@tailwindcss/postcss"],
|
| 3 |
+
};
|
| 4 |
+
|
| 5 |
+
export default config;
|
public/cat.png
ADDED
|
public/favicon.ico
ADDED
|
|
public/file.svg
ADDED
|
|
public/globe.svg
ADDED
|
|
public/next.svg
ADDED
|
|
public/vercel.svg
ADDED
|
|
public/window.svg
ADDED
|
|
src/app/globals.css
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
--background: #ffffff;
|
| 5 |
+
--foreground: #343541;
|
| 6 |
+
--chat-bg: #ffffff;
|
| 7 |
+
--user-bg: #f7f7f8;
|
| 8 |
+
--assistant-bg: #ffffff;
|
| 9 |
+
--border-color: #e5e5e5;
|
| 10 |
+
--cat-primary: #10a37f;
|
| 11 |
+
--cat-primary-hover: #0e8f6f;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
@theme inline {
|
| 15 |
+
--color-background: var(--background);
|
| 16 |
+
--color-foreground: var(--foreground);
|
| 17 |
+
--font-sans: var(--font-geist-sans);
|
| 18 |
+
--font-mono: var(--font-geist-mono);
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
@media (prefers-color-scheme: dark) {
|
| 22 |
+
:root {
|
| 23 |
+
--background: #0a0a0a;
|
| 24 |
+
--foreground: #ededed;
|
| 25 |
+
--chat-bg: #1a1a1a;
|
| 26 |
+
--cat-primary: #10a37f;
|
| 27 |
+
--cat-primary-hover: #0e8f6f;
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
body {
|
| 32 |
+
background: var(--background);
|
| 33 |
+
color: var(--foreground);
|
| 34 |
+
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.prose p {
|
| 38 |
+
margin-top: 0.5em;
|
| 39 |
+
margin-bottom: 0.5em;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.animate-pulse {
|
| 43 |
+
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
@keyframes pulse {
|
| 47 |
+
0%, 100% {
|
| 48 |
+
opacity: 1;
|
| 49 |
+
}
|
| 50 |
+
50% {
|
| 51 |
+
opacity: 0.3;
|
| 52 |
+
}
|
| 53 |
+
}
|
src/app/layout.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
+
import { GoogleAnalytics } from '@next/third-parties/google';
|
| 4 |
+
import "./globals.css";
|
| 5 |
+
|
| 6 |
+
const geistSans = Geist({
|
| 7 |
+
variable: "--font-geist-sans",
|
| 8 |
+
subsets: ["latin"],
|
| 9 |
+
});
|
| 10 |
+
|
| 11 |
+
const geistMono = Geist_Mono({
|
| 12 |
+
variable: "--font-geist-mono",
|
| 13 |
+
subsets: ["latin"],
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
export const metadata: Metadata = {
|
| 17 |
+
title: "CatGPT",
|
| 18 |
+
description: "A cat-powered AI that only says meow",
|
| 19 |
+
icons: {
|
| 20 |
+
icon: '/cat.png',
|
| 21 |
+
apple: '/cat.png',
|
| 22 |
+
},
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
export default function RootLayout({
|
| 26 |
+
children,
|
| 27 |
+
}: Readonly<{
|
| 28 |
+
children: React.ReactNode;
|
| 29 |
+
}>) {
|
| 30 |
+
return (
|
| 31 |
+
<html lang="en">
|
| 32 |
+
<body
|
| 33 |
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
| 34 |
+
>
|
| 35 |
+
{children}
|
| 36 |
+
</body>
|
| 37 |
+
<GoogleAnalytics gaId="G-ZNJBJR6KLN" />
|
| 38 |
+
</html>
|
| 39 |
+
);
|
| 40 |
+
}
|
src/app/page.tsx
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Header from '@/components/Header';
|
| 2 |
+
import Chat from '@/components/Chat';
|
| 3 |
+
|
| 4 |
+
export default function Home() {
|
| 5 |
+
return (
|
| 6 |
+
<div className="flex flex-col h-screen bg-white text-gray-800">
|
| 7 |
+
<Header />
|
| 8 |
+
<main className="flex-1 overflow-hidden border-b border-gray-200">
|
| 9 |
+
<Chat />
|
| 10 |
+
</main>
|
| 11 |
+
</div>
|
| 12 |
+
);
|
| 13 |
+
}
|
src/components/Chat.tsx
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 4 |
+
import Image from 'next/image';
|
| 5 |
+
import ChatMessage from './ChatMessage';
|
| 6 |
+
import ChatInput from './ChatInput';
|
| 7 |
+
|
| 8 |
+
type Message = {
|
| 9 |
+
role: 'user' | 'assistant';
|
| 10 |
+
content: string;
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
// Function to generate a random number of "meow"s
|
| 14 |
+
const generateMeowCount = () => {
|
| 15 |
+
// Random number between 1 and 30
|
| 16 |
+
return Math.floor(Math.random() * 30) + 1;
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
const Chat: React.FC = () => {
|
| 20 |
+
const [messages, setMessages] = useState<Message[]>([
|
| 21 |
+
{
|
| 22 |
+
role: 'assistant',
|
| 23 |
+
content: 'meow',
|
| 24 |
+
},
|
| 25 |
+
]);
|
| 26 |
+
const [isTyping, setIsTyping] = useState(false);
|
| 27 |
+
const [streamingContent, setStreamingContent] = useState('');
|
| 28 |
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 29 |
+
const streamIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
| 30 |
+
|
| 31 |
+
const scrollToBottom = () => {
|
| 32 |
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
useEffect(() => {
|
| 36 |
+
scrollToBottom();
|
| 37 |
+
}, [messages, isTyping, streamingContent]);
|
| 38 |
+
|
| 39 |
+
// Clean up interval on unmount
|
| 40 |
+
useEffect(() => {
|
| 41 |
+
return () => {
|
| 42 |
+
if (streamIntervalRef.current) {
|
| 43 |
+
clearInterval(streamIntervalRef.current);
|
| 44 |
+
}
|
| 45 |
+
};
|
| 46 |
+
}, []);
|
| 47 |
+
|
| 48 |
+
const handleSendMessage = (content: string) => {
|
| 49 |
+
// Clear any existing interval
|
| 50 |
+
if (streamIntervalRef.current) {
|
| 51 |
+
clearInterval(streamIntervalRef.current);
|
| 52 |
+
streamIntervalRef.current = null;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
// Add user message
|
| 56 |
+
setMessages((prev) => [...prev, { role: 'user', content }]);
|
| 57 |
+
|
| 58 |
+
// Simulate typing
|
| 59 |
+
setIsTyping(true);
|
| 60 |
+
setStreamingContent('');
|
| 61 |
+
|
| 62 |
+
// Simulate response delay (between 0.5-1 seconds)
|
| 63 |
+
setTimeout(() => {
|
| 64 |
+
const meowCount = generateMeowCount();
|
| 65 |
+
let currentMeows = 0;
|
| 66 |
+
const meowArray = Array(meowCount).fill("meow");
|
| 67 |
+
|
| 68 |
+
// Start streaming the meows
|
| 69 |
+
streamIntervalRef.current = setInterval(() => {
|
| 70 |
+
if (currentMeows < meowCount) {
|
| 71 |
+
currentMeows++;
|
| 72 |
+
setStreamingContent(meowArray.slice(0, currentMeows).join(" "));
|
| 73 |
+
} else {
|
| 74 |
+
// Finished streaming
|
| 75 |
+
if (streamIntervalRef.current) {
|
| 76 |
+
clearInterval(streamIntervalRef.current);
|
| 77 |
+
streamIntervalRef.current = null;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Add the complete message
|
| 81 |
+
setMessages((prev) => [...prev, {
|
| 82 |
+
role: 'assistant',
|
| 83 |
+
content: meowArray.join(" ")
|
| 84 |
+
}]);
|
| 85 |
+
|
| 86 |
+
setIsTyping(false);
|
| 87 |
+
setStreamingContent('');
|
| 88 |
+
}
|
| 89 |
+
}, 100); // Add a new meow every 100ms
|
| 90 |
+
}, 500 + Math.random() * 500);
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
return (
|
| 94 |
+
<div className="flex flex-col h-full bg-white">
|
| 95 |
+
<div className="flex-1 overflow-y-auto pb-4">
|
| 96 |
+
<div>
|
| 97 |
+
{messages.map((message, index) => (
|
| 98 |
+
<ChatMessage key={index} role={message.role} content={message.content} />
|
| 99 |
+
))}
|
| 100 |
+
{isTyping && (
|
| 101 |
+
<div className="py-6 bg-white">
|
| 102 |
+
<div className="max-w-3xl mx-auto flex items-start gap-4 px-4 sm:px-6 md:px-8">
|
| 103 |
+
<div className="flex-shrink-0 w-8 h-8">
|
| 104 |
+
<div className="w-8 h-8 flex items-center justify-center overflow-hidden">
|
| 105 |
+
<Image src="/cat.png" alt="CatGPT Logo" width={32} height={32} />
|
| 106 |
+
</div>
|
| 107 |
+
</div>
|
| 108 |
+
<div className="flex-1 min-w-0">
|
| 109 |
+
<p className="font-medium text-sm mb-2 text-gray-800">CatGPT</p>
|
| 110 |
+
{streamingContent ? (
|
| 111 |
+
<div className="prose max-w-none text-gray-800">
|
| 112 |
+
<p className="whitespace-pre-wrap">{streamingContent}</p>
|
| 113 |
+
</div>
|
| 114 |
+
) : (
|
| 115 |
+
<div className="flex space-x-2 items-center">
|
| 116 |
+
<span className="w-2 h-2 rounded-full bg-gray-400 animate-pulse"></span>
|
| 117 |
+
<span className="w-2 h-2 rounded-full bg-gray-400 animate-pulse" style={{ animationDelay: '0.2s' }}></span>
|
| 118 |
+
<span className="w-2 h-2 rounded-full bg-gray-400 animate-pulse" style={{ animationDelay: '0.4s' }}></span>
|
| 119 |
+
</div>
|
| 120 |
+
)}
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
)}
|
| 125 |
+
<div ref={messagesEndRef} />
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
<div className="mt-auto">
|
| 129 |
+
<ChatInput onSendMessage={handleSendMessage} disabled={isTyping} />
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
);
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
export default Chat;
|
src/components/ChatInput.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React, { useState, FormEvent } from 'react';
|
| 4 |
+
|
| 5 |
+
type ChatInputProps = {
|
| 6 |
+
onSendMessage: (message: string) => void;
|
| 7 |
+
disabled?: boolean;
|
| 8 |
+
};
|
| 9 |
+
|
| 10 |
+
const ChatInput: React.FC<ChatInputProps> = ({ onSendMessage, disabled = false }) => {
|
| 11 |
+
const [message, setMessage] = useState('');
|
| 12 |
+
|
| 13 |
+
const handleSubmit = (e: FormEvent) => {
|
| 14 |
+
e.preventDefault();
|
| 15 |
+
if (message.trim() && !disabled) {
|
| 16 |
+
onSendMessage(message);
|
| 17 |
+
setMessage('');
|
| 18 |
+
}
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
return (
|
| 22 |
+
<div className="py-4 px-4 sm:px-6 md:px-8 bg-white">
|
| 23 |
+
<form onSubmit={handleSubmit} className="relative max-w-3xl mx-auto">
|
| 24 |
+
<div className="relative shadow-sm rounded-2xl border border-gray-200">
|
| 25 |
+
<textarea
|
| 26 |
+
value={message}
|
| 27 |
+
onChange={(e) => setMessage(e.target.value)}
|
| 28 |
+
placeholder="Ask anything (but I'll only say meow)..."
|
| 29 |
+
disabled={disabled}
|
| 30 |
+
rows={1}
|
| 31 |
+
className="w-full py-3 px-4 pr-12 rounded-2xl border-0 bg-white focus:outline-none resize-none"
|
| 32 |
+
style={{ minHeight: '80px', maxHeight: '200px' }}
|
| 33 |
+
onKeyDown={(e) => {
|
| 34 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 35 |
+
e.preventDefault();
|
| 36 |
+
if (message.trim() && !disabled) {
|
| 37 |
+
onSendMessage(message);
|
| 38 |
+
setMessage('');
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
}}
|
| 42 |
+
/>
|
| 43 |
+
<button
|
| 44 |
+
type="submit"
|
| 45 |
+
disabled={!message.trim() || disabled}
|
| 46 |
+
className={`absolute right-3 bottom-2.5 p-1.5 rounded-full ${
|
| 47 |
+
!message.trim() || disabled
|
| 48 |
+
? 'bg-gray-200 text-gray-500 cursor-not-allowed'
|
| 49 |
+
: 'bg-black text-white hover:bg-gray-800'
|
| 50 |
+
}`}
|
| 51 |
+
>
|
| 52 |
+
<svg
|
| 53 |
+
xmlns="http://www.w3.org/2000/svg"
|
| 54 |
+
width="16"
|
| 55 |
+
height="16"
|
| 56 |
+
viewBox="0 0 24 24"
|
| 57 |
+
fill="none"
|
| 58 |
+
stroke="currentColor"
|
| 59 |
+
strokeWidth="2"
|
| 60 |
+
strokeLinecap="round"
|
| 61 |
+
strokeLinejoin="round"
|
| 62 |
+
>
|
| 63 |
+
<line x1="22" y1="2" x2="11" y2="13"></line>
|
| 64 |
+
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
| 65 |
+
</svg>
|
| 66 |
+
</button>
|
| 67 |
+
</div>
|
| 68 |
+
<div className="text-center text-[8px] text-gray-500 mt-2">
|
| 69 |
+
CatGPT may produce inaccurate information because it's just a cat saying meow. Check out <a href="https://opencharacter.org" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">OpenCharacter</a> for more characters.
|
| 70 |
+
</div>
|
| 71 |
+
</form>
|
| 72 |
+
</div>
|
| 73 |
+
);
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
export default ChatInput;
|
src/components/ChatMessage.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React from 'react';
|
| 4 |
+
import Image from 'next/image';
|
| 5 |
+
|
| 6 |
+
type ChatMessageProps = {
|
| 7 |
+
role: 'user' | 'assistant';
|
| 8 |
+
content: string;
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
const ChatMessage: React.FC<ChatMessageProps> = ({ role, content }) => {
|
| 12 |
+
return (
|
| 13 |
+
<div className={`py-6 ${role === 'user' ? 'bg-[#f7f7f8]' : 'bg-white'}`}>
|
| 14 |
+
<div className="max-w-3xl mx-auto flex items-start gap-4 px-4 sm:px-6 md:px-8">
|
| 15 |
+
<div className="flex-shrink-0 w-8 h-8">
|
| 16 |
+
{role === 'user' ? (
|
| 17 |
+
<div className="w-8 h-8 flex items-center justify-center bg-gray-300">
|
| 18 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
| 19 |
+
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
| 20 |
+
<circle cx="12" cy="7" r="4"></circle>
|
| 21 |
+
</svg>
|
| 22 |
+
</div>
|
| 23 |
+
) : (
|
| 24 |
+
<div className="w-8 h-8 flex items-center justify-center overflow-hidden">
|
| 25 |
+
<Image src="/cat.png" alt="CatGPT Logo" width={32} height={32} />
|
| 26 |
+
</div>
|
| 27 |
+
)}
|
| 28 |
+
</div>
|
| 29 |
+
<div className="flex-1 min-w-0">
|
| 30 |
+
<p className="font-medium text-sm mb-1 text-gray-800">
|
| 31 |
+
{role === 'user' ? 'You' : 'CatGPT'}
|
| 32 |
+
</p>
|
| 33 |
+
<div className="prose max-w-none text-gray-800">
|
| 34 |
+
<p className="whitespace-pre-wrap">{content}</p>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
);
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
export default ChatMessage;
|
src/components/Header.tsx
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import React from 'react';
|
| 4 |
+
|
| 5 |
+
const Header: React.FC = () => {
|
| 6 |
+
return (
|
| 7 |
+
<header className="border-b border-gray-200 py-3 px-4 sm:px-6 md:px-8 bg-white h-12 flex items-center justify-between">
|
| 8 |
+
<div>
|
| 9 |
+
{/* Left side content can go here */}
|
| 10 |
+
</div>
|
| 11 |
+
<div>
|
| 12 |
+
<a
|
| 13 |
+
href="https://opencharacter.org"
|
| 14 |
+
target="_blank"
|
| 15 |
+
rel="noopener noreferrer"
|
| 16 |
+
className="text-sm text-gray-600 hover:text-gray-900 transition-colors"
|
| 17 |
+
>
|
| 18 |
+
Made by OpenCharacter
|
| 19 |
+
</a>
|
| 20 |
+
</div>
|
| 21 |
+
</header>
|
| 22 |
+
);
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
export default Header;
|
tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ES2017",
|
| 4 |
+
"lib": ["dom", "dom.iterable", "esnext"],
|
| 5 |
+
"allowJs": true,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": true,
|
| 8 |
+
"noEmit": true,
|
| 9 |
+
"esModuleInterop": true,
|
| 10 |
+
"module": "esnext",
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"resolveJsonModule": true,
|
| 13 |
+
"isolatedModules": true,
|
| 14 |
+
"jsx": "preserve",
|
| 15 |
+
"incremental": true,
|
| 16 |
+
"plugins": [
|
| 17 |
+
{
|
| 18 |
+
"name": "next"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"paths": {
|
| 22 |
+
"@/*": ["./src/*"]
|
| 23 |
+
}
|
| 24 |
+
},
|
| 25 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
| 26 |
+
"exclude": ["node_modules"]
|
| 27 |
+
}
|