├── .DS_Store ├── app ├── favicon.ico ├── og-image.png ├── assets │ └── hono.png ├── server.ts ├── lib │ ├── utils.ts │ └── snowflake.ts ├── routes │ ├── index.tsx │ ├── api │ │ ├── snow.ts │ │ ├── chat.ts │ │ └── ai.ts │ └── _renderer.tsx ├── types │ └── index.ts ├── client.ts ├── islands │ ├── Header.tsx │ ├── Footer.tsx │ ├── HelperMessage.tsx │ ├── Settings.tsx │ └── Chat.tsx ├── global.d.ts ├── components │ ├── ui │ │ ├── input.tsx │ │ ├── typewriter-effect.tsx │ │ ├── avatar.tsx │ │ ├── scroll-area.tsx │ │ ├── button.tsx │ │ ├── drawer.tsx │ │ └── select.tsx │ ├── message.tsx │ └── Icons.tsx └── tailwind.css ├── postcss.config.js ├── .gitignore ├── wrangler.example.toml ├── components.json ├── tsconfig.json ├── .vscode └── settings.json ├── README.md ├── LICENSE ├── vite.config.ts ├── package.json ├── tailwind.config.js └── snow.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/ohno/HEAD/.DS_Store -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/ohno/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/ohno/HEAD/app/og-image.png -------------------------------------------------------------------------------- /app/assets/hono.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaarthik108/ohno/HEAD/app/assets/hono.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/server.ts: -------------------------------------------------------------------------------- 1 | import { showRoutes } from "hono/dev"; 2 | import { createApp } from "honox/server"; 3 | 4 | const app = createApp(); 5 | 6 | showRoutes(app); 7 | 8 | export default app; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .wrangler 4 | .dev.vars 5 | .hono 6 | 7 | package-lock.json 8 | yarn.lock 9 | pnpm-lock.yaml 10 | bun.lockb 11 | 12 | .env.local 13 | wrangler.toml -------------------------------------------------------------------------------- /app/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /app/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import Chat from "@/islands/Chat"; 2 | import { createRoute } from "honox/factory"; 3 | 4 | export default createRoute(async (c) => { 5 | return c.render( 6 |
7 | 8 |
, 9 | { title: "ohno" } 10 | ); 11 | }); 12 | -------------------------------------------------------------------------------- /app/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface SnowflakeResponse { 2 | choices: Array<{ 3 | messages: string; 4 | }>; 5 | created: number; 6 | model: string; 7 | usage: { 8 | completion_tokens: number; 9 | prompt_tokens: number; 10 | total_tokens: number; 11 | }; 12 | } 13 | export interface RateLimitResponse { 14 | query: string; 15 | } 16 | -------------------------------------------------------------------------------- /wrangler.example.toml: -------------------------------------------------------------------------------- 1 | name = "ohno-template" 2 | compatibility_date = "2024-04-05" 3 | pages_build_output_dir="./dist" 4 | 5 | [placement] 6 | mode = "smart" 7 | 8 | [vars] 9 | OPENAI_API_KEY = "" 10 | GORQ_API_KEY = "" 11 | ACCOUNT = "" 12 | USER_NAME = "" 13 | PASSWORD = "" 14 | ROLE = "" 15 | DATABASE = "" 16 | SCHEMA = "" 17 | WAREHOUSE = "" 18 | SNOWFLAKE_API_URL = "" 19 | TOKEN = "" -------------------------------------------------------------------------------- /app/client.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "honox/client"; 2 | 3 | createClient({ 4 | hydrate: async (elem, root) => { 5 | const { hydrateRoot } = await import("react-dom/client"); 6 | hydrateRoot(root, elem); 7 | }, 8 | createElement: async (type: any, props: any) => { 9 | const { createElement } = await import("react"); 10 | return createElement(type, props); 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/tailwind.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "strict": true, 7 | "lib": ["ESNext", "DOM"], 8 | "types": ["@cloudflare/workers-types", "vite/client"], 9 | "jsx": "react-jsx", 10 | "jsxImportSource": "react", 11 | "baseUrl": ".", 12 | "paths": { 13 | "@/*": ["./app/*"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/islands/Header.tsx: -------------------------------------------------------------------------------- 1 | export function Header() { 2 | return ( 3 |
4 |
5 | 6 |

ohno

7 | 8 | Di1 9 |
10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/islands/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { IconGitHub, IconX } from "@/components/Icons"; 2 | 3 | export default function Footer() { 4 | return ( 5 |
6 |
7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 |
{" "} 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/global.d.ts: -------------------------------------------------------------------------------- 1 | import {} from "hono"; 2 | 3 | import "@hono/react-renderer"; 4 | 5 | declare module "@hono/react-renderer" { 6 | interface Env { 7 | Variables: { 8 | OPENAI_API_KEY: string; 9 | SNOWFLAKE_API_URL: string; 10 | UPSTASH_REDIS_URL: string; 11 | UPSTASH_REDIS_TOKEN: string; 12 | GORQ_API_KEY: string; 13 | TOKEN: string; 14 | X_API_KEY: string; 15 | }; 16 | Bindings: { 17 | kv: KVNamespace; 18 | ai: any; 19 | OPENAI_API_KEY: string; 20 | SNOWFLAKE_API_URL: string; 21 | UPSTASH_REDIS_URL: string; 22 | UPSTASH_REDIS_TOKEN: string; 23 | GORQ_API_KEY: string; 24 | TOKEN: string; 25 | }; 26 | } 27 | interface Props { 28 | title?: string; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#73ecc2", 4 | "activityBar.background": "#73ecc2", 5 | "activityBar.foreground": "#15202b", 6 | "activityBar.inactiveForeground": "#15202b99", 7 | "activityBarBadge.background": "#c273ec", 8 | "activityBarBadge.foreground": "#15202b", 9 | "commandCenter.border": "#15202b99", 10 | "sash.hoverBorder": "#73ecc2", 11 | "statusBar.background": "#46e6af", 12 | "statusBar.foreground": "#15202b", 13 | "statusBarItem.hoverBackground": "#1edb9a", 14 | "statusBarItem.remoteBackground": "#46e6af", 15 | "statusBarItem.remoteForeground": "#15202b", 16 | "titleBar.activeBackground": "#46e6af", 17 | "titleBar.activeForeground": "#15202b", 18 | "titleBar.inactiveBackground": "#46e6af99", 19 | "titleBar.inactiveForeground": "#15202b99" 20 | }, 21 | "peacock.color": "#46e6af", 22 | "CodeGPT.apiKey": "Ollama" 23 | } 24 | -------------------------------------------------------------------------------- /app/lib/snowflake.ts: -------------------------------------------------------------------------------- 1 | import { RateLimitResponse, SnowflakeResponse } from "@/types"; 2 | 3 | export async function executeSnowflakeQuery( 4 | sqlText: string 5 | ): Promise { 6 | const baseUrl = 7 | process.env.NODE_ENV === "production" 8 | ? "https://ohno-1sq.pages.dev" 9 | : "http://localhost:5173"; 10 | 11 | const res = await fetch(`${baseUrl}/api/snow`, { 12 | method: "POST", 13 | headers: { 14 | "Content-Type": "application/json", 15 | }, 16 | body: JSON.stringify({ query: sqlText }), 17 | }); 18 | 19 | if (!res.ok) { 20 | throw new Error("Failed to execute query"); 21 | } 22 | 23 | const data: unknown = await res.json(); 24 | 25 | if ( 26 | typeof data === "object" && 27 | data !== null && 28 | (data as RateLimitResponse).hasOwnProperty("query") 29 | ) { 30 | const rateLimitMessage = (data as RateLimitResponse).query; 31 | throw new Error(rateLimitMessage); 32 | } 33 | 34 | return data as SnowflakeResponse[]; 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ohno 2 | 3 |

4 | ohno - The Fastest AI Chatbot 5 |

6 | 7 | ohno is a lightning-fast AI chatbot built with the Hono.js framework, React, and Tailwind CSS. 8 | 9 | ## Models and Providers 10 | 11 | ohno supports the following AI models and providers: 12 | 13 | - Groq: Llama-3 70B 14 | - Snowflake Cortex: Mixtral 8x7B 15 | - Cloudflare Workers AI: Llama-3 8B 16 | 17 | You can easily switch between models and providers to experiment with different AI models. 18 | 19 | ## Developing 20 | 21 | - Clone the repository 22 | - Install the dependencies: `npm install` 23 | - Configure the credentials in the `wrangler.toml` file 24 | - Deploy api route for snowflake model in a nodejs runtime or run it locally and attach it to a cloudflare tunnel 25 | - Start the development server: `npm run dev` 26 | 27 | ## Credits 28 | 29 | - @yusukebe creator of the [Hono.js]("https://hono.dev/") framework 30 | - @yossydev for the Shadcn Template 31 | 32 | ## License 33 | 34 | ohno is open-source software licensed under the [MIT License](LICENSE). 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Kaarthik Andavar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/islands/HelperMessage.tsx: -------------------------------------------------------------------------------- 1 | import { MoveUpRight } from "lucide-react"; 2 | 3 | interface HelperMessageProps { 4 | onMessageClick: (message: string) => void; 5 | } 6 | const helperMessages = [ 7 | "What can you do?", 8 | "How can you help me?", 9 | "What are your capabilities?", 10 | ]; 11 | 12 | const HelperMessage = ({ onMessageClick }: HelperMessageProps) => { 13 | return ( 14 |
15 | {helperMessages.map((message, index) => ( 16 |
onMessageClick(message)} 20 | > 21 | {message} 22 | 23 |
24 | ))} 25 |
26 | ); 27 | }; 28 | 29 | export default HelperMessage; 30 | -------------------------------------------------------------------------------- /app/components/ui/typewriter-effect.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { motion } from "framer-motion"; 3 | 4 | export const TypewriterEffectSmooth = ({ 5 | words, 6 | className, 7 | }: { 8 | words: { 9 | text: string; 10 | className?: string; 11 | }[]; 12 | className?: string; 13 | }) => { 14 | const containerVariants = { 15 | hidden: { width: 0 }, 16 | visible: { 17 | width: "auto", 18 | transition: { duration: 0.5, ease: "easeInOut" }, 19 | }, 20 | }; 21 | 22 | const renderWords = () => { 23 | return ( 24 |
25 | {words.map((word, idx) => { 26 | return ( 27 | 34 | {word.text} 35 | 36 | ); 37 | })} 38 |
39 | ); 40 | }; 41 | 42 | return ( 43 | 49 |
50 | {renderWords()} 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import pages from "@hono/vite-cloudflare-pages"; 2 | import adapter from "@hono/vite-dev-server/cloudflare"; 3 | import honox from "honox/vite"; 4 | import client from "honox/vite/client"; 5 | import path from "path"; 6 | import { defineConfig } from "vite"; 7 | 8 | export default defineConfig(({ mode }) => { 9 | const common = { 10 | resolve: { 11 | alias: { 12 | "@": path.resolve(__dirname, "./app"), 13 | }, 14 | }, 15 | }; 16 | 17 | if (mode === "client") { 18 | return { 19 | ...common, 20 | plugins: [client({ jsxImportSource: "react" })], 21 | build: { 22 | rollupOptions: { 23 | input: [ 24 | "./app/client.ts", 25 | "/app/tailwind.css", 26 | "/app/favicon.ico", 27 | "/app/og-image.png", 28 | ], 29 | output: { 30 | entryFileNames: "static/client.js", 31 | chunkFileNames: "static/assets/[name]-[hash].js", 32 | assetFileNames: "static/assets/[name].[ext]", 33 | }, 34 | }, 35 | }, 36 | }; 37 | } else { 38 | return { 39 | ...common, 40 | ssr: { 41 | external: [ 42 | "react", 43 | "react-dom", 44 | "openai", 45 | "ai", 46 | "@upstash/ratelimit", 47 | "@upstash/redis/cloudflare", 48 | ], 49 | }, 50 | plugins: [ 51 | honox({ 52 | devServer: { adapter }, 53 | }), 54 | pages(), 55 | ], 56 | }; 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /app/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )) 19 | Avatar.displayName = AvatarPrimitive.Root.displayName 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )) 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )) 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 47 | 48 | export { Avatar, AvatarImage, AvatarFallback } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ohno", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build --mode client && vite build", 7 | "preview": "wrangler pages dev ./dist", 8 | "deploy": "$npm_execpath run build && wrangler pages deploy ./dist" 9 | }, 10 | "private": true, 11 | "dependencies": { 12 | "@hono/react-renderer": "^0.2.0", 13 | "@hono/zod-validator": "^0.2.1", 14 | "@radix-ui/react-avatar": "^1.0.4", 15 | "@radix-ui/react-dialog": "^1.0.5", 16 | "@radix-ui/react-scroll-area": "^1.0.5", 17 | "@radix-ui/react-select": "^2.0.0", 18 | "@radix-ui/react-slot": "^1.0.2", 19 | "@upstash/ratelimit": "^1.1.1", 20 | "ai": "^3.0.23", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.0", 23 | "date-fns": "^3.6.0", 24 | "fetch-event-stream": "^0.1.5", 25 | "framer-motion": "^11.1.5", 26 | "hono": "^4.2.4", 27 | "honox": "^0.1.15", 28 | "lucide-react": "^0.368.0", 29 | "openai": "^4.37.1", 30 | "react": "^18.2.0", 31 | "react-day-picker": "^8.10.0", 32 | "react-dom": "^18.2.0", 33 | "tailwind-merge": "^2.2.2", 34 | "tailwindcss-animate": "^1.0.7", 35 | "vaul": "^0.9.0", 36 | "zod": "^3.22.4" 37 | }, 38 | "devDependencies": { 39 | "@cloudflare/workers-types": "^4.20240208.0", 40 | "@hono/vite-cloudflare-pages": "^0.2.4", 41 | "@types/react": "^18.2.67", 42 | "@types/react-dom": "^18.2.22", 43 | "autoprefixer": "^10.4.19", 44 | "postcss": "^8.4.38", 45 | "tailwindcss": "^3.4.1", 46 | "vite": "^5.0.12", 47 | "wrangler": "^3.51.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/routes/api/snow.ts: -------------------------------------------------------------------------------- 1 | import { MultiRegionRatelimit } from "@upstash/ratelimit"; 2 | import { Redis } from "@upstash/redis/cloudflare"; 3 | import { createRoute } from "honox/factory"; 4 | 5 | const cache = new Map(); 6 | 7 | export const POST = createRoute(async (c) => { 8 | const redis = new Redis({ 9 | url: c.env?.UPSTASH_REDIS_URL as string, 10 | token: c.env?.UPSTASH_REDIS_TOKEN as string, 11 | }); 12 | 13 | const ohnoRateLimit = new MultiRegionRatelimit({ 14 | redis: [redis], 15 | limiter: MultiRegionRatelimit.slidingWindow(5, "60 s"), 16 | analytics: true, 17 | prefix: "ratelimit:ohno", 18 | ephemeralCache: cache, 19 | }); 20 | 21 | const userIP: string = c.req.header("cf-connecting-ip") || "none"; 22 | 23 | const data = await ohnoRateLimit.limit(userIP); 24 | 25 | if (data.success) { 26 | const { query } = await c.req.json(); 27 | 28 | // const baseUrl = c.env?.SNOWFLAKE_API_URL; // cloudflare tunnel 😅 29 | 30 | const baseUrl = `https://snowbrain-agui.vercel.app`; 31 | 32 | const res = await fetch(`${baseUrl}/api/snowai`, { 33 | method: "POST", 34 | headers: { 35 | "Content-Type": "application/json", 36 | "x-api-key": c.env?.X_API_KEY as string, 37 | }, 38 | body: JSON.stringify({ query: query }), 39 | }); 40 | 41 | if (!res.ok) { 42 | throw new Error("Failed to execute query"); 43 | } 44 | 45 | const data = await res.json(); 46 | 47 | return new Response(JSON.stringify(data), { 48 | headers: { 49 | "Content-Type": "application/json", 50 | }, 51 | }); 52 | } else { 53 | return new Response( 54 | JSON.stringify({ 55 | query: "You are rate limited, try again later.", 56 | }), 57 | { status: 200 } 58 | ); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /app/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )) 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )) 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 45 | 46 | export { ScrollArea, ScrollBar } 47 | -------------------------------------------------------------------------------- /app/routes/api/chat.ts: -------------------------------------------------------------------------------- 1 | import { OpenAIStream, StreamingTextResponse } from "ai"; 2 | import { createRoute } from "honox/factory"; 3 | import OpenAI from "openai"; 4 | 5 | export const POST = createRoute(async (c) => { 6 | const { messages } = await c.req.json(); 7 | 8 | const openai = new OpenAI({ 9 | apiKey: c.env?.GORQ_API_KEY as string, 10 | baseURL: "https://api.groq.com/openai/v1", 11 | }); 12 | 13 | try { 14 | const response = await openai.chat.completions.create({ 15 | model: "llama3-70b-8192", 16 | stream: true, 17 | messages, 18 | temperature: 0.7, 19 | max_tokens: 150, 20 | }); 21 | 22 | const stream = OpenAIStream(response); 23 | return new StreamingTextResponse(stream); 24 | } catch (error) { 25 | if ( 26 | typeof error === "object" && 27 | error !== null && 28 | "response" in error && 29 | typeof error.response === "object" && 30 | error.response !== null && 31 | "status" in error.response && 32 | error.response.status === 429 && 33 | "headers" in error.response && 34 | typeof error.response.headers === "object" && 35 | error.response.headers !== null && 36 | "retry-after" in error.response.headers 37 | ) { 38 | const retryAfter = error.response.headers["retry-after"]; 39 | const rateLimitMessage = `Rate limited. Please try again in ${retryAfter} seconds.`; 40 | 41 | return new Response( 42 | JSON.stringify({ 43 | query: rateLimitMessage, 44 | }), 45 | { status: 429 } 46 | ); 47 | } else { 48 | console.error("Error:", error); 49 | return new Response( 50 | JSON.stringify({ 51 | query: "An error occurred while processing your request.", 52 | }), 53 | { status: 500 } 54 | ); 55 | } 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /app/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /app/components/message.tsx: -------------------------------------------------------------------------------- 1 | import hono from "@/assets/hono.png"; 2 | import { cn } from "@/lib/utils"; 3 | import { IconCloudflare, IconUser } from "./Icons"; 4 | 5 | export function BotMessage({ 6 | content, 7 | className, 8 | }: { 9 | content: string; 10 | className?: string; 11 | }) { 12 | return ( 13 |
14 |
15 | Cloudflare 16 |
17 |
18 | {content} 19 |
20 |
21 | ); 22 | } 23 | export function UserMessage({ 24 | content, 25 | className, 26 | }: { 27 | content: string; 28 | className?: string; 29 | }) { 30 | return ( 31 |
32 |
33 | 34 |
35 |
36 | {content} 37 |
38 |
39 | ); 40 | } 41 | export function BotCard({ 42 | children, 43 | showAvatar = true, 44 | }: { 45 | children: React.ReactNode; 46 | showAvatar?: boolean; 47 | }) { 48 | return ( 49 |
50 |
56 | 57 |
58 |
59 | {children} 60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /app/routes/api/ai.ts: -------------------------------------------------------------------------------- 1 | import { createRoute } from "honox/factory"; 2 | 3 | export const POST = createRoute(async (c) => { 4 | const { messages } = await c.req.json(); 5 | 6 | // @ts-ignore 7 | const aiResp = await c.env.AI.run("@cf/meta/llama-3-8b-instruct", { 8 | messages, 9 | stream: true, 10 | }); 11 | let buffer = ""; 12 | const decoder = new TextDecoder(); 13 | const encoder = new TextEncoder(); 14 | const transformer = new TransformStream({ 15 | /* https://github.com/chand1012/openai-cf-workers-ai/blob/main/routes/chat.js */ 16 | transform(chunk, controller) { 17 | buffer += decoder.decode(chunk); 18 | // Process buffered data and try to find the complete message 19 | while (true) { 20 | const newlineIndex = buffer.indexOf("\n"); 21 | if (newlineIndex === -1) { 22 | // If no line breaks are found, it means there is no complete message, wait for the next chunk 23 | break; 24 | } 25 | 26 | const line = buffer.slice(0, newlineIndex + 1); 27 | buffer = buffer.slice(newlineIndex + 1); 28 | 29 | try { 30 | if (line.startsWith("data: ")) { 31 | const content = line.slice("data: ".length); 32 | const doneflag = content.trim() == "[DONE]"; 33 | if (doneflag) { 34 | controller.enqueue(encoder.encode("data: [DONE]\n\n")); 35 | return; 36 | } 37 | 38 | const data = JSON.parse(content); 39 | const newChunk = 40 | "data: " + 41 | JSON.stringify({ 42 | object: "chat.completion.chunk", 43 | choices: [ 44 | { 45 | delta: { content: data.response }, 46 | index: 0, 47 | finish_reason: null, 48 | }, 49 | ], 50 | }) + 51 | "\n\n"; 52 | controller.enqueue(encoder.encode(newChunk)); 53 | } 54 | } catch (err) { 55 | console.error("Error parsing line:", err); 56 | } 57 | } 58 | }, 59 | }); 60 | 61 | return new Response(aiResp.pipeThrough(transformer), { 62 | headers: { 63 | "content-type": "text/event-stream", 64 | "Cache-Control": "no-cache", 65 | Connection: "keep-alive", 66 | }, 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: ["./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}"], 5 | prefix: "", 6 | theme: { 7 | container: { 8 | center: true, 9 | padding: "2rem", 10 | screens: { 11 | "2xl": "1400px", 12 | }, 13 | }, 14 | extend: { 15 | colors: { 16 | border: "hsl(var(--border))", 17 | input: "hsl(var(--input))", 18 | ring: "hsl(var(--ring))", 19 | background: "hsl(var(--background))", 20 | foreground: "hsl(var(--foreground))", 21 | primary: { 22 | DEFAULT: "hsl(var(--primary))", 23 | foreground: "hsl(var(--primary-foreground))", 24 | }, 25 | secondary: { 26 | DEFAULT: "hsl(var(--secondary))", 27 | foreground: "hsl(var(--secondary-foreground))", 28 | }, 29 | destructive: { 30 | DEFAULT: "hsl(var(--destructive))", 31 | foreground: "hsl(var(--destructive-foreground))", 32 | }, 33 | muted: { 34 | DEFAULT: "hsl(var(--muted))", 35 | foreground: "hsl(var(--muted-foreground))", 36 | }, 37 | accent: { 38 | DEFAULT: "hsl(var(--accent))", 39 | foreground: "hsl(var(--accent-foreground))", 40 | }, 41 | popover: { 42 | DEFAULT: "hsl(var(--popover))", 43 | foreground: "hsl(var(--popover-foreground))", 44 | }, 45 | card: { 46 | DEFAULT: "hsl(var(--card))", 47 | foreground: "hsl(var(--card-foreground))", 48 | }, 49 | }, 50 | borderRadius: { 51 | lg: "var(--radius)", 52 | md: "calc(var(--radius) - 2px)", 53 | sm: "calc(var(--radius) - 4px)", 54 | }, 55 | keyframes: { 56 | "accordion-down": { 57 | from: { height: "0" }, 58 | to: { height: "var(--radix-accordion-content-height)" }, 59 | }, 60 | "accordion-up": { 61 | from: { height: "var(--radix-accordion-content-height)" }, 62 | to: { height: "0" }, 63 | }, 64 | }, 65 | animation: { 66 | "accordion-down": "accordion-down 0.2s ease-out", 67 | "accordion-up": "accordion-up 0.2s ease-out", 68 | }, 69 | }, 70 | }, 71 | plugins: [require("tailwindcss-animate")], 72 | }; 73 | -------------------------------------------------------------------------------- /app/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | 78 | .loading-animation { 79 | position: relative; 80 | overflow: hidden; 81 | } 82 | 83 | .loading-bar { 84 | height: 12px; 85 | background-color: #e0e0e0; 86 | border-radius: 6px; 87 | position: relative; 88 | overflow: hidden; 89 | } 90 | 91 | .loading-bar:nth-child(1) { 92 | width: 50%; 93 | } 94 | 95 | .loading-bar:nth-child(2) { 96 | width: 75%; 97 | } 98 | 99 | .loading-bar:nth-child(3) { 100 | width: 50%; 101 | } 102 | 103 | .loading-bar:nth-child(4) { 104 | width: 75%; 105 | } 106 | 107 | .loading-bar:nth-child(5) { 108 | width: 50%; 109 | } 110 | 111 | .loading-bar::before { 112 | content: ""; 113 | position: absolute; 114 | top: 0; 115 | left: 0; 116 | width: 100%; 117 | height: 100%; 118 | background-color: #c0c0c0; 119 | transform: translateX(-100%); 120 | animation: loading 1.5s infinite; 121 | } 122 | 123 | @keyframes loading { 124 | 0% { 125 | transform: translateX(-100%); 126 | } 127 | 100% { 128 | transform: translateX(100%); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /snow.ts: -------------------------------------------------------------------------------- 1 | /* Deploy this on a nodejs runtime or simply run in local and connect to cloudflare tunnels */ 2 | 3 | // import { env } from "hono/adapter"; 4 | // import { createRoute } from "honox/factory"; 5 | // import snowflake from "snowflake-sdk"; 6 | 7 | // export const POST = createRoute(async (c) => { 8 | // const token = c.req.header("Authorization"); 9 | 10 | // if (!token || !token.startsWith("Bearer ")) { 11 | // return new Response("Unauthorized", { 12 | // status: 401, 13 | // headers: { "Content-Type": "application/json" }, 14 | // }); 15 | // } 16 | 17 | // const bearerToken = token.substring(7); // Remove "Bearer " prefix 18 | 19 | // if (bearerToken !== env<{ TOKEN: string }>(c).TOKEN) { 20 | // return new Response("Unauthorized", { 21 | // status: 401, 22 | // headers: { "Content-Type": "application/json" }, 23 | // }); 24 | // } 25 | 26 | // const requestBody = await c.req.json(); 27 | // console.log(requestBody); 28 | 29 | // const config = env<{ 30 | // ACCOUNT: string; 31 | // USER_NAME: string; 32 | // PASSWORD: string; 33 | // ROLE: string; 34 | // WAREHOUSE: string; 35 | // DATABASE: string; 36 | // SCHEMA: string; 37 | // }>(c); 38 | 39 | // const snowConnect = snowflake.createConnection({ 40 | // account: config.ACCOUNT, 41 | // username: config.USER_NAME, 42 | // password: config.PASSWORD, 43 | // role: config.ROLE, 44 | // warehouse: config.WAREHOUSE, 45 | // database: config.DATABASE, 46 | // schema: config.SCHEMA, 47 | // }); 48 | 49 | // snowflake.configure({ ocspFailOpen: false }); 50 | 51 | // const query = requestBody.query; 52 | 53 | // try { 54 | // const result = await new Promise((resolve, reject) => { 55 | // snowConnect.connect((err, conn) => { 56 | // if (err) { 57 | // console.error("Unable to connect: " + err.message); 58 | // reject(err); 59 | // } else { 60 | // snowConnect.execute({ 61 | // sqlText: query, 62 | // complete: (err, stmt, rows) => { 63 | // if (err) { 64 | // console.error( 65 | // "Failed to execute statement due to the following error: " + 66 | // err.message 67 | // ); 68 | // reject(err); 69 | // } else { 70 | // resolve(rows || []); 71 | // } 72 | // }, 73 | // }); 74 | // } 75 | // }); 76 | // }); 77 | 78 | // // const columns = result.length > 0 ? Object.keys(result[0]) : []; 79 | // // const formattedResult = { 80 | // // columns, 81 | // // data: result, 82 | // // }; 83 | 84 | // return new Response(JSON.stringify(result), { 85 | // status: 200, 86 | // headers: { "Content-Type": "application/json" }, 87 | // }); 88 | // } catch (error) { 89 | // console.error(error); 90 | // return new Response(JSON.stringify({ error: error }), { 91 | // status: 500, 92 | // headers: { "Content-Type": "application/json" }, 93 | // }); 94 | // } 95 | // }); 96 | -------------------------------------------------------------------------------- /app/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Drawer as DrawerPrimitive } from "vaul" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Drawer = ({ 7 | shouldScaleBackground = true, 8 | ...props 9 | }: React.ComponentProps) => ( 10 | 14 | ) 15 | Drawer.displayName = "Drawer" 16 | 17 | const DrawerTrigger = DrawerPrimitive.Trigger 18 | 19 | const DrawerPortal = DrawerPrimitive.Portal 20 | 21 | const DrawerClose = DrawerPrimitive.Close 22 | 23 | const DrawerOverlay = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName 34 | 35 | const DrawerContent = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, children, ...props }, ref) => ( 39 | 40 | 41 | 49 |
50 | {children} 51 | 52 | 53 | )) 54 | DrawerContent.displayName = "DrawerContent" 55 | 56 | const DrawerHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
64 | ) 65 | DrawerHeader.displayName = "DrawerHeader" 66 | 67 | const DrawerFooter = ({ 68 | className, 69 | ...props 70 | }: React.HTMLAttributes) => ( 71 |
75 | ) 76 | DrawerFooter.displayName = "DrawerFooter" 77 | 78 | const DrawerTitle = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 90 | )) 91 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName 92 | 93 | const DrawerDescription = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, ...props }, ref) => ( 97 | 102 | )) 103 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName 104 | 105 | export { 106 | Drawer, 107 | DrawerPortal, 108 | DrawerOverlay, 109 | DrawerTrigger, 110 | DrawerClose, 111 | DrawerContent, 112 | DrawerHeader, 113 | DrawerFooter, 114 | DrawerTitle, 115 | DrawerDescription, 116 | } 117 | -------------------------------------------------------------------------------- /app/islands/Settings.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Drawer, 4 | DrawerClose, 5 | DrawerContent, 6 | DrawerDescription, 7 | DrawerFooter, 8 | DrawerHeader, 9 | DrawerTitle, 10 | DrawerTrigger, 11 | } from "@/components/ui/drawer"; 12 | import { 13 | Select, 14 | SelectContent, 15 | SelectGroup, 16 | SelectItem, 17 | SelectLabel, 18 | SelectTrigger, 19 | SelectValue, 20 | } from "@/components/ui/select"; 21 | import { Settings2 } from "lucide-react"; 22 | import { useState } from "react"; 23 | 24 | interface SettingsProps { 25 | selectedModel: "groq" | "snowflake" | "cloudflare"; 26 | setSelectedModel: (model: "groq" | "snowflake" | "cloudflare") => void; 27 | systemMessage: string; 28 | setSystemMessage: (message: string) => void; 29 | setMessages: ( 30 | messages: { id: string; role: string; content: string }[] 31 | ) => void; 32 | } 33 | 34 | export const Settings: React.FC = ({ 35 | selectedModel, 36 | setSelectedModel, 37 | systemMessage, 38 | setSystemMessage, 39 | setMessages, 40 | }) => { 41 | const [isDrawerOpen, setIsDrawerOpen] = useState(false); 42 | 43 | return ( 44 | 45 | 46 | 52 | 53 | 54 |
55 | 56 | Settings 57 | 58 | Configure the AI model and system message. 59 | 60 | 61 |
62 | 65 | 84 |
85 | 88 | 95 |
96 |
97 | 98 | 109 | 110 | 113 | 114 | 115 |
116 |
117 |
118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /app/routes/_renderer.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/islands/Footer"; 2 | import { Header } from "@/islands/Header"; 3 | import { reactRenderer, useRequestContext } from "@hono/react-renderer"; 4 | import { FC, PropsWithChildren } from "react"; 5 | 6 | const HasIslands: FC = ({ children }) => { 7 | const IMPORTING_ISLANDS_ID = "__importing_islands" as const; 8 | const c = useRequestContext(); 9 | return <>{c.get(IMPORTING_ISLANDS_ID) ? children : <>}; 10 | }; 11 | 12 | export default reactRenderer(({ children, title }) => { 13 | return ( 14 | <> 15 | {/* */} 16 | 17 | 18 | 19 | 23 | 24 | 25 | {title || "Chatbot Template | Hono.js | Snowflake Cortex LLM"} 26 | 27 | 31 | 35 | 36 | 43 | 47 | 48 | 52 | 56 | 57 | 63 | 67 | 71 | 72 | {import.meta.env.PROD ? ( 73 | <> 74 | 75 | 76 | 77 | 78 | 79 | ) : ( 80 | <> 81 | 82 | 83 | 84 | )} 85 | {title ? {title} : ""} 86 | 87 | 88 |
89 | {children} 90 |