NeoPy commited on
Commit
b9ee0e6
·
verified ·
1 Parent(s): 40a6d4d

Upload folder using huggingface_hub

Browse files
.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
- title: Catchat
3
- emoji: 🐢
4
- colorFrom: blue
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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&apos;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
+ }