├── src ├── vite-env.d.ts ├── main.tsx ├── App.tsx ├── index.css ├── components │ ├── CustomLink.tsx │ ├── WelcomeScreen.tsx │ ├── ChunkRenderer.tsx │ ├── ToolCallIndicator.tsx │ ├── ChunkedMarkdown.tsx │ ├── CodeBlock.tsx │ ├── CombinedSearchIndicator.tsx │ └── Chat.tsx ├── App.css ├── lib │ └── utils.ts └── styles.module.css ├── public ├── favicon.png └── cloudflare.webp ├── worker ├── config.ts ├── ai.ts ├── index.ts └── prompt.ts ├── vite.config.ts ├── tsconfig.worker.json ├── tsconfig.json ├── index.html ├── .gitignore ├── eslint.config.js ├── tsconfig.node.json ├── tsconfig.app.json ├── package.json ├── wrangler.jsonc └── README.md /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-with-groq/groq-docs-chat/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/cloudflare.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/build-with-groq/groq-docs-chat/HEAD/public/cloudflare.webp -------------------------------------------------------------------------------- /worker/config.ts: -------------------------------------------------------------------------------- 1 | export const DOCS_BASE_URL = "https://console.groq.com"; 2 | export const DOCS_BASE_HOST = new URL(DOCS_BASE_URL).hostname; -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | import { cloudflare } from "@cloudflare/vite-plugin"; 5 | 6 | // https://vite.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react(), cloudflare()], 9 | }) -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /tsconfig.worker.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.node.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo", 5 | "types": ["./worker-configuration.d.ts", "vite/client"], 6 | }, 7 | "include": ["./worker-configuration.d.ts", "./worker"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.app.json" 6 | }, 7 | { 8 | "path": "./tsconfig.node.json" 9 | }, 10 | { 11 | "path": "./tsconfig.worker.json" 12 | } 13 | ], 14 | "compilerOptions": { 15 | "types": [ 16 | "./worker-configuration.d.ts" 17 | ] 18 | } 19 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Groq AI Docs Chat 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # wrangler files 27 | .wrangler 28 | .dev.vars* 29 | !.dev.vars.example 30 | .env* 31 | !.env.example 32 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | import Chat from './components/Chat' 3 | import { Github } from 'lucide-react' 4 | 5 | function App() { 6 | return ( 7 | <> 8 | 9 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default App 23 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | display: flex; 19 | place-items: center; 20 | min-width: 320px; 21 | min-height: 100vh; 22 | } 23 | -------------------------------------------------------------------------------- /worker/ai.ts: -------------------------------------------------------------------------------- 1 | import { createGroq } from '@ai-sdk/groq'; 2 | import { createAiGateway } from 'ai-gateway-provider'; 3 | import { env } from 'cloudflare:workers'; 4 | 5 | export const aigateway = createAiGateway({ 6 | accountId: env.CLOUDFLARE_ACCOUNT_ID!, 7 | gateway: env.CLOUDFLARE_AI_GATEWAY_ID!, 8 | apiKey: env.CLOUDFLARE_AI_GATEWAY_API_KEY, 9 | }); 10 | 11 | export const groq = createGroq({ 12 | apiKey: env.GROQ_API_KEY ?? "", 13 | baseURL: env.GROQ_BASE_URL || undefined 14 | }); 15 | 16 | export const model = aigateway([ 17 | groq('openai/gpt-oss-20b'), 18 | groq('llama-3.1-8b-instant') 19 | ]); -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { globalIgnores } from 'eslint/config' 7 | 8 | export default tseslint.config([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs['recommended-latest'], 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /src/components/CustomLink.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import styles from "../styles.module.css"; 3 | 4 | /** 5 | * A styled anchor element that opens https links in a new tab. 6 | */ 7 | const CustomLink = memo(function CustomLink({ 8 | href, 9 | children, 10 | }: { 11 | href?: string; 12 | children: React.ReactNode; 13 | }) { 14 | const isHttps = href?.startsWith("https"); 15 | 16 | return ( 17 | 23 | {children} 24 | 25 | ); 26 | }); 27 | 28 | export default CustomLink; -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "erasableSyntaxOnly": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /src/components/WelcomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import styles from "../styles.module.css"; 2 | 3 | export function WelcomeScreen() { 4 | return ( 5 |
6 |
7 |
8 | Groq 9 |
10 |
11 | Cloudflare 12 |
13 |
14 |
15 |

Chat with Docs - Groq x Cloudflare

16 |

17 | Chat with any website using Groq's fast inference and Cloudflare's AI Search. Ask any question about Groq and get answers directly from the documentation. 18 |

19 |
20 |
Start typing to begin your conversation
21 |
22 | ) 23 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "groq-docs-chat", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "npm run build && vite preview", 11 | "deploy": "npm run build && wrangler deploy", 12 | "cf-typegen": "wrangler types" 13 | }, 14 | "dependencies": { 15 | "@ai-sdk/groq": "^1.2.9", 16 | "@ai-sdk/react": "^1.2.12", 17 | "ai": "^4.3.19", 18 | "ai-gateway-provider": "^0.0.11", 19 | "clsx": "^2.1.1", 20 | "lucide-react": "^0.544.0", 21 | "prism-react-renderer": "^2.4.1", 22 | "react": "^19.1.1", 23 | "react-dom": "^19.1.1", 24 | "react-markdown": "^10.1.0", 25 | "remark-gfm": "^4.0.1", 26 | "zod": "^3.25.76" 27 | }, 28 | "devDependencies": { 29 | "@cloudflare/vite-plugin": "^1.13.4", 30 | "@eslint/js": "^9.33.0", 31 | "@types/react": "^19.1.10", 32 | "@types/react-dom": "^19.1.7", 33 | "@vitejs/plugin-react": "^5.0.0", 34 | "eslint": "^9.33.0", 35 | "eslint-plugin-react-hooks": "^5.2.0", 36 | "eslint-plugin-react-refresh": "^0.4.20", 37 | "globals": "^16.3.0", 38 | "typescript": "~5.8.3", 39 | "typescript-eslint": "^8.39.1", 40 | "vite": "^7.1.2", 41 | "wrangler": "^4.39.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Brand Colors */ 3 | --primary-color: #F55036; 4 | --primary-dark: #d43e2a; 5 | --primary-darker: #b8341f; 6 | --primary-light: #f76b55; 7 | --primary-lightest: rgba(245, 80, 54, 0.1); 8 | 9 | /* Background Colors */ 10 | --background-color: transparent; 11 | --background-surface-color: transparent; 12 | 13 | /* Text Colors */ 14 | --text-color: #ffffff; 15 | --text-secondary: #c0c3c9; 16 | 17 | /* Emphasis Colors */ 18 | --emphasis-100: #3a3b3c; 19 | --emphasis-200: #4e4f50; 20 | --emphasis-300: #6a6b6c; 21 | --emphasis-700: #b0b3b8; 22 | --emphasis-900: #e4e6ea; 23 | 24 | /* Font */ 25 | --font-family-base: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 26 | 27 | /* Navbar height for layout calculations */ 28 | --navbar-height: 60px; 29 | } 30 | 31 | #root { 32 | max-width: 1280px; 33 | width: 100%; 34 | margin: 0 auto; 35 | margin-top: auto; 36 | } 37 | 38 | .github-link { 39 | position: fixed; 40 | top: 1rem; 41 | right: 1rem; 42 | z-index: 1000; 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | width: 48px; 47 | height: 48px; 48 | background: var(--emphasis-200); 49 | border-radius: 50%; 50 | color: var(--text-color); 51 | text-decoration: none; 52 | transition: all 0.2s ease-in-out; 53 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 54 | } 55 | 56 | .github-link:hover { 57 | background: var(--emphasis-300); 58 | transform: scale(1.05); 59 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 60 | } 61 | 62 | .github-link:active { 63 | transform: scale(0.95); 64 | } -------------------------------------------------------------------------------- /src/components/ChunkRenderer.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useMemo } from "react"; 2 | import ReactMarkdown from "react-markdown"; 3 | import CodeBlock from "./CodeBlock"; 4 | import CustomLink from "./CustomLink"; 5 | import remarkGfm from "remark-gfm"; 6 | import styles from "../styles.module.css"; 7 | 8 | /** 9 | * Individual chunk renderer that memoizes its content to prevent unnecessary re-renders 10 | */ 11 | const ChunkRenderer = memo(function ChunkRenderer({ 12 | chunk, 13 | index, 14 | }: { 15 | chunk: string; 16 | index: number; 17 | }) { 18 | const markdownComponents = useMemo( 19 | () => ({ 20 | code: ({ node, className, children, ...props }: any) => { 21 | const match = /language-(\w+)/.exec(className || ""); 22 | return match ? ( 23 | 24 | {String(children).replace(/\n$/, "")} 25 | 26 | ) : ( 27 | 28 | {children} 29 | 30 | ); 31 | }, 32 | a: ({ href, children }: any) => ( 33 | {children} 34 | ), 35 | }), 36 | [] 37 | ); 38 | 39 | return ( 40 |
41 | 45 | {chunk} 46 | 47 |
48 | ); 49 | }); 50 | 51 | export default ChunkRenderer; -------------------------------------------------------------------------------- /wrangler.jsonc: -------------------------------------------------------------------------------- 1 | /** 2 | * For more details on how to configure Wrangler, refer to: 3 | * https://developers.cloudflare.com/workers/wrangler/configuration/ 4 | */ 5 | { 6 | "$schema": "node_modules/wrangler/config-schema.json", 7 | "name": "groq-docs-chat", 8 | "main": "worker/index.ts", 9 | "compatibility_date": "2025-09-24", 10 | "assets": { 11 | "not_found_handling": "single-page-application" 12 | }, 13 | "observability": { 14 | "enabled": true 15 | }, 16 | "ai": { 17 | "binding": "AI" 18 | }, 19 | "vars": { 20 | "AI_SEARCH_NAME": "groq-docs-search" 21 | } 22 | /** 23 | * Smart Placement 24 | * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement 25 | */ 26 | // "placement": { "mode": "smart" } 27 | /** 28 | * Bindings 29 | * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including 30 | * databases, object storage, AI inference, real-time communication and more. 31 | * https://developers.cloudflare.com/workers/runtime-apis/bindings/ 32 | */ 33 | /** 34 | * Environment Variables 35 | * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables 36 | */ 37 | // "vars": { "MY_VARIABLE": "production_value" } 38 | /** 39 | * Note: Use secrets to store sensitive data. 40 | * https://developers.cloudflare.com/workers/configuration/secrets/ 41 | */ 42 | /** 43 | * Static Assets 44 | * https://developers.cloudflare.com/workers/static-assets/binding/ 45 | */ 46 | // "assets": { "directory": "./public/", "binding": "ASSETS" } 47 | /** 48 | * Service Bindings (communicate between multiple Workers) 49 | * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings 50 | */ 51 | // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] 52 | } -------------------------------------------------------------------------------- /src/components/ToolCallIndicator.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import styles from "../styles.module.css"; 3 | 4 | 5 | 6 | /** 7 | * Renders a tool call indicator for non-search tools 8 | */ 9 | export default function ToolCallIndicator({ 10 | toolCall, 11 | isExpanded, 12 | onToggle, 13 | isCurrentlyLoading, 14 | }: { 15 | toolCall: any; 16 | isExpanded: boolean; 17 | onToggle: () => void; 18 | isCurrentlyLoading?: boolean; 19 | }) { 20 | // For other tool calls, show a generic indicator 21 | if (toolCall.result) { 22 | return ( 23 |
24 |
{ 30 | if (e.key === "Enter" || e.key === " ") { 31 | onToggle(); 32 | } 33 | }} 34 | > 35 | Completed {toolCall.toolName} {isExpanded ? "▼" : "▶"} 36 |
37 | {isExpanded && ( 38 |
39 |
40 | Tool: 41 | 42 | {toolCall.toolName} 43 | 44 |
45 |
46 | Status: 47 | Completed 48 |
49 |
50 | )} 51 |
52 | ); 53 | } 54 | 55 | // Only show "Running..." indicator if we're currently loading 56 | if (isCurrentlyLoading) { 57 | return ( 58 |
59 | Running {toolCall.toolName}... 60 |
61 | ); 62 | } 63 | 64 | // If not loading and no result, don't show anything 65 | return null; 66 | } -------------------------------------------------------------------------------- /src/components/ChunkedMarkdown.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { useState } from "react"; 3 | import { useEffect } from "react"; 4 | import { splitIntoChunks } from "../lib/utils"; 5 | import styles from "../styles.module.css"; 6 | import ChunkRenderer from "./ChunkRenderer"; 7 | 8 | /** 9 | * Optimized markdown renderer that renders content in chunks to avoid re-processing completed content 10 | */ 11 | const ChunkedMarkdown = memo(function ChunkedMarkdown({ 12 | content, 13 | isStreaming, 14 | }: { 15 | content: string; 16 | isStreaming: boolean; 17 | }) { 18 | const [processedChunks, setProcessedChunks] = useState([]); 19 | const [pendingContent, setPendingContent] = useState(""); 20 | 21 | useEffect(() => { 22 | if (!content) { 23 | setProcessedChunks([]); 24 | setPendingContent(""); 25 | return; 26 | } 27 | 28 | const chunks = splitIntoChunks(content); 29 | 30 | if (!isStreaming) { 31 | // When not streaming, process all chunks immediately 32 | setProcessedChunks(chunks); 33 | setPendingContent(""); 34 | } else { 35 | // When streaming, only process complete chunks 36 | const completeChunks = chunks.slice(0, -1); 37 | const lastChunk = chunks[chunks.length - 1] || ""; 38 | 39 | // Check if we have new complete chunks 40 | if (completeChunks.length > processedChunks.length) { 41 | setProcessedChunks(completeChunks); 42 | } 43 | 44 | // Set pending content (the incomplete last chunk) 45 | setPendingContent(lastChunk); 46 | } 47 | }, [content, isStreaming, processedChunks.length]); 48 | 49 | return ( 50 |
51 | {/* Render completed chunks (these never re-render) */} 52 | {processedChunks.map((chunk, index) => ( 53 | 54 | ))} 55 | 56 | {/* Render pending content during streaming */} 57 | {isStreaming && pendingContent && ( 58 |
59 | 60 | {pendingContent} 61 | 62 |
63 | )} 64 |
65 | ); 66 | }); 67 | 68 | export default ChunkedMarkdown; -------------------------------------------------------------------------------- /src/components/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { Highlight, themes } from "prism-react-renderer"; 2 | import { useState } from "react"; 3 | import styles from "../styles.module.css"; 4 | import { Check, Copy } from "lucide-react"; 5 | 6 | 7 | /** 8 | * Renders a syntax-highlighted code block using Prism. 9 | * Accepts content and a className like `language-lua`. 10 | */ 11 | export default function CodeBlock({ 12 | children, 13 | className, 14 | }: { 15 | children: string; 16 | className?: string; 17 | }) { 18 | const language = className?.replace(/language-/, "") || "text"; 19 | const [copied, setCopied] = useState(false); 20 | 21 | // Map common language aliases to Prism language names 22 | const languageMap: { [key: string]: string } = { 23 | js: "javascript", 24 | ts: "typescript", 25 | python: "python", 26 | sh: "bash", 27 | html: "html", 28 | css: "css", 29 | json: "json", 30 | yaml: "yaml", 31 | markdown: "markdown", 32 | txt: "text", 33 | }; 34 | 35 | const prismLanguage = languageMap[language.toLowerCase()] || "text"; 36 | 37 | const handleCopy = async () => { 38 | try { 39 | await navigator.clipboard.writeText(children.trim()); 40 | setCopied(true); 41 | setTimeout(() => setCopied(false), 2000); 42 | } catch (err) { 43 | console.error("Failed to copy text: ", err); 44 | } 45 | }; 46 | 47 | return ( 48 |
49 | 54 | {({ className, style, tokens, getLineProps, getTokenProps }) => ( 55 |
56 |                         {tokens.map((line, i) => (
57 |                             
58 | {line.map((token, key) => ( 59 | 60 | ))} 61 |
62 | ))} 63 |
64 | )} 65 |
66 | 73 |
74 | ); 75 | } -------------------------------------------------------------------------------- /worker/index.ts: -------------------------------------------------------------------------------- 1 | import { type CoreMessage, streamText } from 'ai'; 2 | import { z } from 'zod'; 3 | import { tool } from 'ai'; 4 | import { SYSTEM_PROMPT } from './prompt'; 5 | import { model } from './ai'; 6 | import { DOCS_BASE_HOST } from './config'; 7 | 8 | export default { 9 | async fetch(request, env) { 10 | const url = new URL(request.url); 11 | 12 | // Handle CORS preflight requests 13 | if (request.method === 'OPTIONS') { 14 | return new Response(null, { 15 | status: 204, 16 | headers: { 17 | 'Access-Control-Allow-Origin': '*', 18 | 'Access-Control-Allow-Methods': 'POST, OPTIONS', 19 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, Accept, Origin', 20 | 'Access-Control-Max-Age': '86400' 21 | } 22 | }); 23 | } 24 | 25 | // Only accept POST requests 26 | if (request.method !== 'POST') { 27 | return new Response(null, { status: 405 }); 28 | } 29 | 30 | // Only accept requests to chat 31 | if (url.pathname !== "/api/chat") { 32 | return new Response(null, { status: 404 }); 33 | } 34 | 35 | 36 | const ai = env.AI; 37 | if (!ai) { 38 | return new Response('AI not defined', { status: 500 }); 39 | } 40 | 41 | const { messages } = (await request.json()) as { messages: CoreMessage[] }; 42 | 43 | const result = streamText({ 44 | system: SYSTEM_PROMPT, 45 | model, 46 | temperature: 0, 47 | maxRetries: 2, 48 | maxTokens: 8192, 49 | maxSteps: 5, 50 | messages, 51 | tools: { 52 | search_docs: tool({ 53 | description: 'Search the Groq documentation. Returns a string with the search results of relevant documentation pages.', 54 | parameters: z.object({ 55 | query: z.string().describe('The query to search the documentation for, such as "responses api" or "structured outputs"') 56 | }), 57 | execute: async ({ query }) => { 58 | const searchResponse = await ai.autorag(env.AI_SEARCH_NAME).search({ 59 | query, 60 | rewrite_query: false, 61 | max_num_results: 10, 62 | ranking_options: { 63 | score_threshold: 0.4 64 | } 65 | }); 66 | 67 | if (searchResponse.data.length === 0) { 68 | return ''; 69 | } 70 | 71 | return searchResponse.data.map((item) => { 72 | const url = new URL(item.filename); 73 | url.hostname = DOCS_BASE_HOST; 74 | return `${item.content.map((content) => content.text).join('\n\n')}`; 75 | }).join('\n\n'); 76 | }, 77 | }), 78 | }, 79 | // onFinish: (result) => { 80 | // console.log('Finished:', result); 81 | // }, 82 | onError: (error) => { 83 | console.error(error); 84 | }, 85 | }) 86 | 87 | return result.toDataStreamResponse(); 88 | }, 89 | } satisfies ExportedHandler; 90 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Splits text into logical chunks for incremental rendering 4 | */ 5 | export function splitIntoChunks(text: string): string[] { 6 | if (!text) return []; 7 | 8 | // Split by double newlines (paragraphs) and code blocks 9 | const chunks = []; 10 | let currentChunk = ""; 11 | let inCodeBlock = false; 12 | 13 | const lines = text.split("\n"); 14 | 15 | for (let i = 0; i < lines.length; i++) { 16 | const line = lines[i]; 17 | 18 | // Detect code block boundaries 19 | if (line.trim().startsWith("```")) { 20 | inCodeBlock = !inCodeBlock; 21 | currentChunk += line + "\n"; 22 | 23 | // If we're ending a code block, complete this chunk 24 | if (!inCodeBlock) { 25 | chunks.push(currentChunk.trim()); 26 | currentChunk = ""; 27 | } 28 | continue; 29 | } 30 | 31 | currentChunk += line + "\n"; 32 | 33 | // If not in code block and we hit an empty line or end, complete chunk 34 | if (!inCodeBlock && (line.trim() === "" || i === lines.length - 1)) { 35 | if (currentChunk.trim()) { 36 | chunks.push(currentChunk.trim()); 37 | currentChunk = ""; 38 | } 39 | } 40 | } 41 | 42 | // Add any remaining content 43 | if (currentChunk.trim()) { 44 | chunks.push(currentChunk.trim()); 45 | } 46 | 47 | return chunks; 48 | } 49 | 50 | 51 | /** 52 | * Counts the number of file references in a tool response 53 | */ 54 | export function countFilesInResponse(response: string): number { 55 | const matches = response.match(/ tool.toolName === "search_docs" 84 | ); 85 | const otherTools = toolInvocations.filter( 86 | (tool) => tool.toolName !== "search_docs" 87 | ); 88 | 89 | const totalSearchedDocs = searchDocs.reduce((total, tool) => { 90 | return total + (tool.result ? countFilesInResponse(tool.result) : 0); 91 | }, 0); 92 | 93 | return { 94 | searchDocs, 95 | otherTools, 96 | totalSearchedDocs, 97 | hasCompletedSearches: searchDocs.some((tool) => tool.result), 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Groq Documentation Chat 2 | 3 | **AI-powered documentation assistant that helps developers build AI applications with Groq's fast inference platform** 4 | 5 | 6 | 7 | https://github.com/user-attachments/assets/b95d9232-f82e-45c4-8044-94ce75ef8925 8 | 9 | 10 | ## Live Demo 11 | 12 | [**Try it out now!**](https://groq-docs-chat.groqcloud.dev/) 13 | 14 | 15 | ## Overview 16 | 17 | This application demonstrates intelligent documentation search and assistance using [Groq's API](https://console.groq.com/home) for lightning-fast AI responses with [Cloudflare's AI Search capabilities](https://developers.cloudflare.com/ai-search/). Built as a complete, end-to-end template that you can fork, customize, and deploy to help users navigate and understand AI documentation. 18 | 19 | **Key Features:** 20 | - Real-time AI-powered documentation search using [Cloudflare AI Search](https://developers.cloudflare.com/ai-search/) + [Groq Inference](https://console.groq.com/home) 21 | - Interactive chat interface with streaming responses 22 | - Tool-based architecture with expandable search indicators 23 | - Chunked markdown rendering for better UX during streaming 24 | - Sub-second response times, efficient concurrent request handling, and production-grade performance powered by Groq 25 | 26 | ## Architecture 27 | 28 | **Tech Stack:** 29 | - **Frontend:** React 19, TypeScript, Vite 30 | - **Backend:** Cloudflare Workers with AI Search 31 | - **AI Infrastructure:** Groq API with multiple model fallbacks, powered by [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/usage/providers/groq/) 32 | - **Deployment:** Cloudflare Workers 33 | - **Styling:** CSS Modules with custom components 34 | 35 | ## Quick Start 36 | 37 | ### Prerequisites 38 | - Node.js 20+ and npm 39 | - Groq API key ([Create a free GroqCloud account and generate an API key here](https://console.groq.com/keys)) 40 | - Cloudflare account with Workers and AI enabled 41 | 42 | ### Setup 43 | 44 | 1. **Clone the repository** 45 | ```bash 46 | git clone https://github.com/build-with-groq/groq-docs-chat 47 | cd groq-docs-chat 48 | ``` 49 | 50 | 2. **Install dependencies** 51 | ```bash 52 | npm install 53 | ``` 54 | 55 | 3. **Create a Cloudflare AI Gateway and enable Groq as a Provider:** 56 | a. [Cloudflare AI Gateway + Groq documentation](https://developers.cloudflare.com/ai-gateway/usage/providers/groq/) 57 | b. Get a [Groq API Key](https://console.groq.com/keys) 58 | 59 | 4. [**Create a Cloudflare AI Search**](https://developers.cloudflare.com/ai-search/) 60 | 61 | 3. **Configure environment variables** 62 | Create a `.env.local` file or set these in your Cloudflare Worker: 63 | ```bash 64 | GROQ_API_KEY=your_groq_api_key_here 65 | CLOUDFLARE_ACCOUNT_ID=your_cloudflare_account_id 66 | CLOUDFLARE_AI_GATEWAY_ID=your_ai_gateway_id 67 | AI_SEARCH_NAME=your_ai_search_search_name 68 | ``` 69 | 70 | 4. **Development** 71 | ```bash 72 | npm run dev 73 | ``` 74 | 75 | 5. **Deploy to Cloudflare** 76 | ```bash 77 | npm run deploy 78 | ``` 79 | 80 | ## Customization 81 | This template is designed to be a foundation for you to get started with. Key areas for customization: 82 | - **Model Selection:** Update Groq model configuration in `worker/ai.ts:17-18` 83 | - **Search Integration:** Modify AI Search parameters in `worker/index.ts:58-65` 84 | - **UI/Styling:** Customize themes and components in `src/styles.module.css` 85 | - **System Prompt:** Adjust AI behavior in `worker/prompt.ts` 86 | 87 | ## Next Steps 88 | ### For Developers 89 | - **Create your free GroqCloud account:** Access official API docs, the playground for experimentation, and more resources via [Groq Console](https://console.groq.com). 90 | - **Build and customize:** Fork this repo and start customizing to build out your own AI-powered documentation assistant. 91 | - **Get support:** Connect with other developers building on Groq, chat with our team, and submit feature requests on our [Groq Developer Forum](https://community.groq.com). 92 | ### For Founders and Business Leaders 93 | - **See enterprise capabilities:** This template showcases production-ready AI that can handle realtime business workloads. Explore other applications and use cases. 94 | - **Discuss Your needs:** [Contact our team](https://groq.com/enterprise-access/) to explore how Groq can accelerate your AI initiatives. 95 | 96 | 97 | ## License 98 | This project is licensed under the MIT License - see the LICENSE file for details. 99 | 100 | ## Credits 101 | Created by [Ben Ankiel](https://www.linkedin.com/in/ben-ankiel). 102 | -------------------------------------------------------------------------------- /src/components/CombinedSearchIndicator.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import styles from "../styles.module.css"; 3 | import { extractFileNamesFromResponse } from "../lib/utils"; 4 | 5 | /** 6 | * Component for displaying combined search results 7 | */ 8 | export default function CombinedSearchIndicator({ 9 | searchTools, 10 | totalDocs, 11 | hasCompleted, 12 | isExpanded, 13 | onToggle, 14 | isCurrentlyLoading, 15 | }: { 16 | searchTools: any[]; 17 | totalDocs: number; 18 | hasCompleted: boolean; 19 | isExpanded: boolean; 20 | onToggle: () => void; 21 | isCurrentlyLoading?: boolean; 22 | }) { 23 | const hasRunningSearches = searchTools.some((tool) => !tool.result); 24 | 25 | // If any searches are still running AND we're currently loading, show loading state 26 | if (hasRunningSearches && isCurrentlyLoading) { 27 | return ( 28 |
29 |
30 | Reading docs... 31 |
32 |
33 | ); 34 | } 35 | 36 | // If all searches are completed 37 | if (hasCompleted) { 38 | return ( 39 |
40 |
{ 46 | if (e.key === "Enter" || e.key === " ") { 47 | onToggle(); 48 | } 49 | }} 50 | > 51 | Read {totalDocs} docs 52 |
53 | {isExpanded && ( 54 |
55 | {searchTools.map((tool, index) => { 56 | const toolPages = tool.result 57 | ? extractFileNamesFromResponse(tool.result) 58 | : []; 59 | return ( 60 |
61 |
62 | Search: 63 | 64 | "{tool.args?.query || "N/A"}" 65 | 66 |
67 | {toolPages.length > 0 && ( 68 |
69 | Pages: 70 |
71 | {[...new Set(toolPages)].map((pageUrl, pageIndex) => { 72 | // Extract the page name from the URL for display 73 | const pageName = new URL(pageUrl).pathname; 74 | return ( 75 | 88 | ); 89 | })} 90 |
91 |
92 | )} 93 |
94 | ); 95 | })} 96 |
97 | )} 98 |
99 | ); 100 | } 101 | 102 | return null; 103 | } -------------------------------------------------------------------------------- /src/components/Chat.tsx: -------------------------------------------------------------------------------- 1 | import { useChat } from "@ai-sdk/react"; 2 | import clsx from "clsx"; 3 | import { useCallback, useEffect, useRef, useState } from "react"; 4 | import styles from "../styles.module.css"; 5 | import { groupToolCalls } from "../lib/utils"; 6 | import CombinedSearchIndicator from "./CombinedSearchIndicator"; 7 | import ToolCallIndicator from "./ToolCallIndicator"; 8 | import { ArrowRight, LoaderCircle } from 'lucide-react' 9 | import ChunkedMarkdown from "./ChunkedMarkdown"; 10 | import { WelcomeScreen } from "./WelcomeScreen"; 11 | 12 | export default function Chat() { 13 | const { messages, input, handleInputChange, handleSubmit, status, stop } = 14 | useChat({ 15 | onError: (error) => { 16 | console.error(error); 17 | }, 18 | }); 19 | 20 | const messagesEndRef = useRef(null); 21 | const messagesContainerRef = useRef(null); 22 | const [expandedToolCalls, setExpandedToolCalls] = useState>( 23 | new Set() 24 | ); 25 | const [shouldAutoScroll, setShouldAutoScroll] = useState(true); 26 | 27 | const scrollToBottom = useCallback(() => { 28 | if (shouldAutoScroll && messagesEndRef.current) { 29 | messagesEndRef.current.scrollIntoView({ behavior: "instant" }); 30 | } 31 | }, [shouldAutoScroll]); 32 | 33 | const toggleToolCall = useCallback((toolCallId: string) => { 34 | setExpandedToolCalls((prev) => { 35 | const newSet = new Set(prev); 36 | if (newSet.has(toolCallId)) { 37 | newSet.delete(toolCallId); 38 | } else { 39 | newSet.add(toolCallId); 40 | } 41 | return newSet; 42 | }); 43 | }, []); 44 | 45 | const handleButtonClick = useCallback( 46 | (e: React.MouseEvent) => { 47 | if (status === "streaming") { 48 | e.preventDefault(); 49 | stop(); 50 | } 51 | // If not loading, the form's onSubmit will handle the submission 52 | }, 53 | [status, stop] 54 | ); 55 | 56 | // Check if user has scrolled up from the bottom 57 | const handleScroll = useCallback(() => { 58 | if (messagesContainerRef.current) { 59 | const { scrollTop, scrollHeight, clientHeight } = 60 | messagesContainerRef.current; 61 | const isAtBottom = scrollTop + clientHeight >= scrollHeight - 100; // 100px threshold 62 | setShouldAutoScroll(isAtBottom); 63 | } 64 | }, []); 65 | 66 | useEffect(() => { 67 | scrollToBottom(); 68 | }, [messages, scrollToBottom]); 69 | 70 | return ( 71 |
72 |
77 | {messages.length === 0 && } 78 | 79 | {messages.map((message) => ( 80 |
89 |
90 | {message.role === "user" ? "You" : "Groq"} 91 |
92 |
93 | {/* Render tool invocations first */} 94 | {message.toolInvocations && 95 | (() => { 96 | const { 97 | searchDocs, 98 | otherTools, 99 | totalSearchedDocs, 100 | hasCompletedSearches, 101 | } = groupToolCalls(message.toolInvocations); 102 | const searchId = `${message.id}-search`; 103 | const isLastMessage = 104 | message.id === messages[messages.length - 1]?.id; 105 | const isCurrentlyLoading = isLastMessage && status === "streaming"; 106 | 107 | return ( 108 | <> 109 | {/* Combined search docs indicator */} 110 | {searchDocs.length > 0 && ( 111 | toggleToolCall(searchId)} 117 | isCurrentlyLoading={isCurrentlyLoading} 118 | /> 119 | )} 120 | 121 | {/* Other tool calls */} 122 | {otherTools.map((toolInvocation, i) => { 123 | const toolCallId = `${message.id}-other-${i}`; 124 | return ( 125 | toggleToolCall(toolCallId)} 130 | isCurrentlyLoading={isCurrentlyLoading} 131 | /> 132 | ); 133 | })} 134 | 135 | ); 136 | })()} 137 | 138 | {/* Render message parts */} 139 | {message.parts?.map((part, i) => { 140 | switch (part.type) { 141 | case "text": 142 | const isLastMessage = 143 | message.id === messages[messages.length - 1]?.id; 144 | const isStreamingMessage = isLastMessage && status === "streaming"; 145 | 146 | return ( 147 |
151 | {message.role === "user" ? ( 152 |

{part.text}

153 | ) : ( 154 | 158 | )} 159 |
160 | ); 161 | default: 162 | return null; 163 | } 164 | })} 165 |
166 |
167 | ))} 168 | 169 | {status === "streaming" && 170 | messages.length > 0 && 171 | messages[messages.length - 1].role === "user" && ( 172 |
173 |
Groq
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | )} 185 | 186 |
187 |
188 | 189 |
190 |
191 | 199 | 217 |
218 |
219 |
220 | ); 221 | } -------------------------------------------------------------------------------- /worker/prompt.ts: -------------------------------------------------------------------------------- 1 | export const SYSTEM_PROMPT = ` 2 | You are a helpful assistant named Groq. 3 | Explain things clearly and concisely. 4 | Your only job is to help answer questions about Groq, an AI inference platform that enables you to build AI powered apps. 5 | 6 | It powers GroqCloud™, a full-stack platform for fast, affordable, scalable, and production-ready inference. 7 | 8 | 9 | You are an expert on the Groq API, which enables anyone to create their own AI powered apps with Groq's fast and affordable inference, powered by our custom LPU™. 10 | 11 | In addition to the user query, you will also be given a list of files that you can use to answer the question. These are documentation files that have detailed information about the Groq API. Do not make up any information, only use the information in the files. 12 | Use the search_docs tool to search the documentation for relevant information. You must search the documentation once and only once. After that, you should answer the question based on the information you have. 13 | Use markdown to format your responses. Always link to the documentation files when you can, or if no documentation file is relevant, you can link to https://console.groq.com, or the main website at https://groq.com. Include these links when relevant. 14 | Always use proper capitalization and punctuation. 15 | You should be conversational and not always agree with the user. 16 | When you are given documentation, you should not reference the files as "the documentation files", but instead simply use it to answer the question, and include links to the pages in the documentation. 17 | Do not include code in snippets unless it comes directly from the documentation. If a method does not exist in the documentation, you should not include it in the snippet. 18 | After giving an example, include a list to relevant documentation pages with quick descriptions so the user can learn more. 19 | If you do not know how to do something, you should use the search_docs tool to search the documentation for relevant information. Search once, then respond using that information. If you still do not know how to do something, explain what you do know how to do, and make sure that the user knows that there might be missing information. Never use psuedo code or make up information. Explain where the user can find more information and provide links to the documentation. 20 | Always add inline links to the documentation when relevant. Format all links as regular markdown links to the documentation pages. 21 | If they ask a complicated question, break it down and ask them up to three clarifying questions before answering or searching the documentation. 22 | Never end with a signature or other filler. 23 | Always add descriptive comments in code with links to relevant documentation pages. 24 | Always validate assumptions with the documentation provided. 25 | Always use the search_docs tool to search the documentation at least once before answering the user's question, even if it seems like it's not related to Groq API. 26 | Never add placeholders in your responses. Only include real code and API from the documentation. 27 | If you do not understand the user's question, you should ask them to clarify. 28 | 29 | 30 | Groq's main AI capabilities include: 31 | - Text generation: given a prompt, a large language model generates text 32 | - Speech to text: takes input media files and transcribes them into text 33 | - Text to speech: takes text and converts it into speech audio clips 34 | - Built in tools: Groq's Compound systems can use web search, code execution, Wolfram Alpha, and more 35 | 36 | The only official Groq SDKs are: 37 | - groq-sdk for JavaScript and TypeScript 38 | - groq for Python 39 | 40 | Code snippet examples (Responses API): 41 | 42 | // JavaScript 43 | import OpenAI from "openai"; 44 | const client = new OpenAI({ 45 | apiKey: process.env.GROQ_API_KEY, 46 | baseURL: "https://api.groq.com/openai/v1", 47 | }); 48 | 49 | const response = await client.responses.create({ 50 | model: "openai/gpt-oss-20b", 51 | input: "Explain the importance of fast language models", 52 | }); 53 | console.log(response.output_text); 54 | 55 | 56 | # Python 57 | 58 | from openai import OpenAI 59 | import os 60 | client = OpenAI( 61 | api_key=os.environ.get("GROQ_API_KEY"), 62 | base_url="https://api.groq.com/openai/v1", 63 | ) 64 | 65 | response = client.responses.create( 66 | input="Explain the importance of fast language models", 67 | model="openai/gpt-oss-20b", 68 | ) 69 | print(response.output_text) 70 | 71 | 72 | // Curl 73 | curl -X POST https://api.groq.com/openai/v1/responses \ 74 | -H "Authorization: Bearer $GROQ_API_KEY" \ 75 | -H "Content-Type: application/json" \ 76 | -d '{ 77 | "model": "openai/gpt-oss-20b", 78 | "input": "Explain the importance of fast language models" 79 | }' 80 | 81 | 82 | Non-responses api examples: 83 | 84 | // JavaScript 85 | import Groq from "groq-sdk"; 86 | 87 | const groq = new Groq(); 88 | 89 | export async function main() { 90 | const completion = await getGroqChatCompletion(); 91 | console.log(completion.choices[0]?.message?.content || ""); 92 | } 93 | 94 | export const getGroqChatCompletion = async () => { 95 | return groq.chat.completions.create({ 96 | messages: [ 97 | // Set an optional system message. This sets the behavior of the 98 | // assistant and can be used to provide specific instructions for 99 | // how it should behave throughout the conversation. 100 | { 101 | role: "system", 102 | content: "You are a helpful assistant.", 103 | }, 104 | // Set a user message for the assistant to respond to. 105 | { 106 | role: "user", 107 | content: "Explain the importance of fast language models", 108 | }, 109 | ], 110 | model: "openai/gpt-oss-20b", 111 | }); 112 | }; 113 | 114 | main(); 115 | 116 | 117 | // Python 118 | from groq import Groq 119 | 120 | client = Groq() 121 | 122 | chat_completion = client.chat.completions.create( 123 | messages=[ 124 | # Set an optional system message. This sets the behavior of the 125 | # assistant and can be used to provide specific instructions for 126 | # how it should behave throughout the conversation. 127 | { 128 | "role": "system", 129 | "content": "You are a helpful assistant." 130 | }, 131 | # Set a user message for the assistant to respond to. 132 | { 133 | "role": "user", 134 | "content": "Explain the importance of fast language models", 135 | } 136 | ], 137 | 138 | # The language model which will generate the completion. 139 | model="llama-3.3-70b-versatile" 140 | ) 141 | 142 | # Print the completion returned by the LLM. 143 | print(chat_completion.choices[0].message.content) 144 | 145 | 146 | 147 | Documentation pages for reference: 148 | - Overview: Introduces Groq's fast, OpenAI-compatible LLM inference platform for building AI applications 149 | - Quickstart: Step-by-step guide for creating API keys, setting up environment, and making first requests 150 | - OpenAI Compatibility: Details how Groq maintains compatibility with OpenAI API standards 151 | - Responses API: Documentation for Groq's enhanced response generation API with improved features 152 | - Models: Overview of supported AI models on GroqCloud with capabilities and performance characteristics 153 | - Rate Limits: Information about API usage limits and how to manage request throttling 154 | - Examples: Collection of sample applications and templates demonstrating Groq's AI capabilities 155 | - Text Generation: Guide for using Groq's language models for text completion and generation tasks 156 | - Speech to Text: Documentation for transcribing audio files into text using Groq's models 157 | - Text to Speech: Instructions for converting text into speech audio clips 158 | - Images and Vision: Guide for processing and analyzing images with vision-capable models 159 | - Reasoning: Documentation for models with advanced reasoning and problem-solving capabilities 160 | - Structured Outputs: How to generate JSON and other structured data formats from models 161 | - Web Search: Built-in tool for searching the web and retrieving current information 162 | - Browser Search: Advanced web search capabilities with browser automation features 163 | - Visit Website: Tool for accessing and extracting information from specific websites 164 | - Browser Automation: Automated web browsing and interaction capabilities 165 | - Code Execution: Secure sandboxed Python code execution for calculations and programming tasks 166 | - Wolfram Alpha: Integration with computational intelligence for mathematical and factual queries 167 | - Compound Overview: Introduction to Groq's compound AI systems combining multiple capabilities 168 | - Compound Systems: Advanced AI systems that intelligently use multiple tools and resources 169 | - Compound Built-in Tools: Documentation for integrated tools like search, code execution, and computation 170 | - Compound Use Cases: Real-world applications and examples of compound AI systems 171 | - Batch Processing: Efficient processing of multiple requests in batches for cost optimization 172 | - Flex Processing: Flexible processing options for different performance and cost requirements 173 | - Content Moderation: Tools and guidelines for filtering and moderating AI-generated content 174 | - Prefilling: Technique for optimizing response generation by providing context prefixes 175 | - Tool Use: Comprehensive guide for integrating external APIs and tools with AI models 176 | - Remote MCP: Model Context Protocol for connecting to remote data sources and services 177 | - LoRA Inference: Low-Rank Adaptation inference for efficient model fine-tuning and deployment 178 | - Prompt Basics: Fundamental principles and best practices for crafting effective prompts 179 | - Prompt Patterns: Advanced prompting techniques and design patterns for better results 180 | - Model Migration: Guide for transitioning between different models and API versions 181 | - Prompt Caching: Optimization technique for faster response times through prompt caching 182 | - Optimizing Latency: Best practices for reducing response times and improving performance 183 | - Groq Libraries: Official Python and JavaScript client libraries with installation and usage examples 184 | - Integrations Catalog: Third-party tools and platforms that integrate with Groq's services 185 | - Spend Limits: Managing costs and setting usage limits for API consumption 186 | - Projects: Organization and management of API keys and resources within projects 187 | - Billing FAQs: Common questions and answers about pricing and billing processes 188 | - Your Data: Information about data handling, privacy, and security practices 189 | - Developer Community: Join our developer community to connect with other developers and get help with your projects at https://community.groq.com/ 190 | - Errors: Common error codes, troubleshooting guides, and resolution strategies 191 | - Changelog: Release notes and updates for API changes and new features 192 | - Policies and Notices: Terms of service, privacy policies, and legal information 193 | 194 | When creating examples in NodeJS, you should use ES6 syntax in NodeJS 22+. NodeJS includes a built in fetch, so you should use that instead of a library. Use top level await. 195 | 196 | More information in case the user asks specific questions: 197 | 198 | Get support by selecting your profile at the top right and clicking "Chat with us" or email support@groq.com 199 | 200 | Need higher rate limits? Fill out this form and we'll get back to you: https://groq.com/self-serve-support 201 | 202 | Need to scale? Apply for enterprise access: https://groq.com/enterprise-access 203 | 204 | Need data governance or regional endpoints? That's available for enterprise customers: https://groq.com/enterprise-access 205 | 206 | `; -------------------------------------------------------------------------------- /src/styles.module.css: -------------------------------------------------------------------------------- 1 | .chatContainer { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | width: 100%; 6 | max-width: 1000px; 7 | margin: 0 auto; 8 | background: var(--background-color); 9 | } 10 | 11 | .messagesContainer { 12 | flex: 1; 13 | overflow-y: auto; 14 | padding: 1rem; 15 | display: flex; 16 | flex-direction: column; 17 | gap: 1rem; 18 | scroll-behavior: smooth; 19 | /* Performance optimizations */ 20 | will-change: scroll-position; 21 | contain: layout style paint; 22 | } 23 | 24 | /* Custom scrollbar styling with transparent background */ 25 | .messagesContainer::-webkit-scrollbar { 26 | width: 8px; 27 | } 28 | 29 | .messagesContainer::-webkit-scrollbar-track { 30 | background: transparent; 31 | } 32 | 33 | .messagesContainer::-webkit-scrollbar-thumb { 34 | background: var(--emphasis-300); 35 | border-radius: 4px; 36 | } 37 | 38 | .messagesContainer::-webkit-scrollbar-thumb:hover { 39 | background: var(--emphasis-700); 40 | } 41 | 42 | .message { 43 | display: flex; 44 | flex-direction: column; 45 | max-width: 80%; 46 | word-wrap: break-word; 47 | } 48 | 49 | .userMessage { 50 | align-self: flex-end; 51 | } 52 | 53 | .assistantMessage { 54 | align-self: flex-start; 55 | } 56 | 57 | .messageContent { 58 | padding: 0.75rem 1rem; 59 | border-radius: 1rem; 60 | margin: 0.25rem 0; 61 | line-height: 1.5; 62 | } 63 | 64 | .userMessage .messageContent { 65 | background: var(--primary-color); 66 | color: white; 67 | border-bottom-right-radius: 0.25rem; 68 | } 69 | 70 | .assistantMessage .messageContent { 71 | background: var(--emphasis-100); 72 | color: var(--text-color); 73 | border-bottom-left-radius: 0.25rem; 74 | } 75 | 76 | .messageRole { 77 | font-size: 0.75rem; 78 | color: var(--text-secondary); 79 | margin-bottom: 0.25rem; 80 | font-weight: 500; 81 | } 82 | 83 | .userMessage .messageRole { 84 | text-align: right; 85 | } 86 | 87 | .inputContainer { 88 | padding: 1rem; 89 | border-top: 1px solid var(--emphasis-200); 90 | background: var(--background-color); 91 | } 92 | 93 | .inputForm { 94 | display: flex; 95 | gap: 0.5rem; 96 | max-width: 100%; 97 | } 98 | 99 | .input { 100 | flex: 1; 101 | padding: 0.75rem 1rem; 102 | border: 1px solid var(--emphasis-300); 103 | border-radius: 1.5rem; 104 | background: var(--background-surface-color); 105 | color: var(--text-color); 106 | outline: none; 107 | font-size: 1rem; 108 | transition: border-color 0.2s ease; 109 | } 110 | 111 | .input:focus { 112 | border-color: var(--primary-color); 113 | box-shadow: 0 0 0 2px var(--primary-lightest); 114 | } 115 | 116 | .input::placeholder { 117 | color: var(--text-secondary); 118 | } 119 | 120 | .sendButton { 121 | padding: 0.75rem 0.75rem; 122 | background: var(--primary-color); 123 | color: white; 124 | border: none; 125 | border-radius: 1.5rem; 126 | font-weight: 500; 127 | cursor: pointer; 128 | transition: background-color 0.2s ease, transform 0.1s ease; 129 | display: flex; 130 | align-items: center; 131 | gap: 0.5rem; 132 | } 133 | 134 | .sendButton:hover { 135 | background: var(--primary-dark); 136 | transform: translateY(-1px); 137 | } 138 | 139 | .sendButton:active { 140 | transform: translateY(0); 141 | } 142 | 143 | .sendButton:disabled { 144 | opacity: 0.6; 145 | cursor: not-allowed; 146 | transform: none; 147 | } 148 | 149 | .markdownContent { 150 | line-height: 1.6; 151 | /* Performance optimizations for markdown rendering */ 152 | contain: layout style; 153 | transform: translateZ(0); 154 | /* Force hardware acceleration */ 155 | } 156 | 157 | .markdownContent p { 158 | margin: 0.5rem 0; 159 | } 160 | 161 | .markdownContent p:first-child { 162 | margin-top: 0; 163 | } 164 | 165 | .markdownContent p:last-child { 166 | margin-bottom: 0; 167 | } 168 | 169 | .markdownContent pre { 170 | background: var(--emphasis-100); 171 | padding: 0.75rem; 172 | border-radius: 0.5rem; 173 | overflow-x: auto; 174 | margin: 0.5rem 0; 175 | } 176 | 177 | .markdownContent code { 178 | background: var(--emphasis-100); 179 | padding: 0.125rem 0.25rem; 180 | border-radius: 0.25rem; 181 | font-size: 0.875em; 182 | } 183 | 184 | .markdownContent pre code { 185 | background: none; 186 | padding: 0; 187 | } 188 | 189 | .markdownContent ul, 190 | .markdownContent ol { 191 | margin: 0.5rem 0; 192 | padding-left: 1.5rem; 193 | } 194 | 195 | .markdownContent blockquote { 196 | border-left: 3px solid var(--primary-color); 197 | padding-left: 1rem; 198 | margin: 0.5rem 0; 199 | color: var(--text-secondary); 200 | } 201 | 202 | .markdownLink { 203 | color: var(--text-color); 204 | text-decoration: none; 205 | border-bottom: 1px solid var(--primary-color); 206 | transition: all 0.2s ease; 207 | font-weight: 500; 208 | } 209 | 210 | .markdownLink:hover { 211 | color: var(--text-color); 212 | border-bottom: 2px solid var(--primary-dark); 213 | text-decoration: none; 214 | } 215 | 216 | .markdownLink:visited { 217 | color: var(--text-color); 218 | border-bottom-color: var(--primary-color); 219 | } 220 | 221 | .assistantMessage .markdownLink { 222 | color: var(--text-color); 223 | border-bottom-color: var(--primary-color); 224 | } 225 | 226 | .userMessage .markdownLink { 227 | color: rgba(255, 255, 255, 0.9); 228 | border-bottom-color: var(--primary-color); 229 | } 230 | 231 | .userMessage .markdownLink:hover { 232 | color: white; 233 | border-bottom: 2px solid var(--primary-dark); 234 | } 235 | 236 | /* Loading animation styles */ 237 | .loadingDots { 238 | display: inline-flex; 239 | gap: 0.25rem; 240 | align-items: center; 241 | } 242 | 243 | .loadingDot { 244 | width: 0.5rem; 245 | height: 0.5rem; 246 | background: var(--text-secondary); 247 | border-radius: 50%; 248 | animation: loadingPulse 1.4s ease-in-out infinite both; 249 | } 250 | 251 | .loadingDot:nth-child(1) { 252 | animation-delay: -0.32s; 253 | } 254 | 255 | .loadingDot:nth-child(2) { 256 | animation-delay: -0.16s; 257 | } 258 | 259 | .loadingDot:nth-child(3) { 260 | animation-delay: 0s; 261 | } 262 | 263 | @keyframes loadingPulse { 264 | 265 | 0%, 266 | 80%, 267 | 100% { 268 | transform: scale(0.6); 269 | opacity: 0.5; 270 | } 271 | 272 | 40% { 273 | transform: scale(1); 274 | opacity: 1; 275 | } 276 | } 277 | 278 | /* Spinner animation for submit button */ 279 | .spinner { 280 | animation: spin 1s linear infinite; 281 | } 282 | 283 | @keyframes spin { 284 | from { 285 | transform: rotate(0deg); 286 | } 287 | 288 | to { 289 | transform: rotate(360deg); 290 | } 291 | } 292 | 293 | /* Loading indicator with stop square */ 294 | .loadingIndicator { 295 | position: relative; 296 | display: flex; 297 | align-items: center; 298 | justify-content: center; 299 | } 300 | 301 | .stopSquare { 302 | position: absolute; 303 | width: 8px; 304 | height: 8px; 305 | background: white; 306 | border-radius: 1px; 307 | top: 50%; 308 | left: 50%; 309 | transform: translate(-50%, -50%); 310 | z-index: 1; 311 | } 312 | 313 | .loadingButton { 314 | cursor: pointer !important; 315 | opacity: 1 !important; 316 | } 317 | 318 | .loadingButton:hover { 319 | background: var(--primary-darker) !important; 320 | transform: translateY(-1px) !important; 321 | } 322 | 323 | pre { 324 | padding: 0 !important; 325 | margin-block: 0.25rem; 326 | } 327 | 328 | /* Code block with copy button styles */ 329 | .codeBlockContainer { 330 | position: relative; 331 | } 332 | 333 | .codeBlockContainer pre { 334 | margin: 0 !important; 335 | padding: 0.75rem 0.5rem !important; 336 | border-radius: 0.5rem; 337 | overflow-x: auto; 338 | } 339 | 340 | .copyButton { 341 | position: absolute; 342 | top: 0.5rem; 343 | right: 0.5rem; 344 | background: var(--emphasis-200); 345 | border: 1px solid var(--emphasis-300); 346 | border-radius: 0.375rem; 347 | padding: 0.375rem; 348 | cursor: pointer; 349 | display: flex; 350 | align-items: center; 351 | justify-content: center; 352 | opacity: 0.7; 353 | transition: all 0.2s ease; 354 | color: var(--text-color); 355 | } 356 | 357 | .copyButton:hover { 358 | opacity: 1; 359 | background: var(--emphasis-300); 360 | transform: scale(1.05); 361 | } 362 | 363 | .copyButton:active { 364 | transform: scale(0.95); 365 | } 366 | 367 | /* Tool call indicator styles */ 368 | .toolCall { 369 | color: var(--text-color); 370 | opacity: 0.8; 371 | font-size: 0.875rem; 372 | font-style: italic; 373 | position: relative; 374 | overflow: hidden; 375 | text-decoration: underline; 376 | text-decoration-style: dotted; 377 | text-decoration-color: var(--primary-color); 378 | } 379 | 380 | .toolCallShimmer { 381 | /* Apply shimmer only to text using background-clip and text-fill-color */ 382 | background: linear-gradient(90deg, 383 | var(--emphasis-900) 25%, 384 | var(--emphasis-700) 50%, 385 | var(--emphasis-900) 75%); 386 | background-size: 200% 100%; 387 | animation: shimmer 1.5s infinite linear; 388 | -webkit-background-clip: text; 389 | -webkit-text-fill-color: transparent; 390 | background-clip: text; 391 | color: transparent; 392 | } 393 | 394 | .toolCallCompleted { 395 | color: var(--text-color); 396 | cursor: pointer; 397 | transition: all 0.2s ease; 398 | text-decoration: underline; 399 | text-decoration-style: dotted; 400 | text-decoration-color: var(--primary-color); 401 | } 402 | 403 | .toolCallCompleted:hover { 404 | color: var(--text-color); 405 | text-decoration-color: var(--primary-dark); 406 | } 407 | 408 | /* Tool call container and inline details */ 409 | .toolCallContainer { 410 | margin: 0.25rem 0; 411 | } 412 | 413 | .toolDetails { 414 | margin-top: 0.25rem; 415 | padding: 0.5rem; 416 | background: var(--emphasis-100); 417 | border-bottom-right-radius: 0.5rem; 418 | border-top-right-radius: 0.5rem; 419 | border-left: 3px solid var(--primary-color); 420 | } 421 | 422 | .toolDetailItem { 423 | display: flex; 424 | align-items: flex-start; 425 | margin-bottom: 0.25rem; 426 | } 427 | 428 | .toolDetailItem:last-child { 429 | margin-bottom: 0; 430 | } 431 | 432 | .toolDetailLabel { 433 | font-weight: 600; 434 | color: var(--text-color); 435 | min-width: 80px; 436 | font-size: 0.8rem; 437 | } 438 | 439 | .toolDetailValue { 440 | color: var(--text-secondary); 441 | font-size: 0.8rem; 442 | flex: 1; 443 | } 444 | 445 | .toolDetailSection { 446 | padding-bottom: 0.5rem; 447 | margin-bottom: 0.5rem; 448 | border-bottom: 1px solid var(--emphasis-200); 449 | } 450 | 451 | .toolDetailSection:last-child { 452 | padding-bottom: 0; 453 | margin-bottom: 0; 454 | border-bottom: none; 455 | } 456 | 457 | .foundPageItem { 458 | margin-bottom: 0.25rem; 459 | } 460 | 461 | .foundPageItem:last-child { 462 | margin-bottom: 0; 463 | } 464 | 465 | .foundPageLink { 466 | color: var(--text-color); 467 | text-decoration: none; 468 | border-bottom: 1px solid var(--primary-color); 469 | font-size: 0.8rem; 470 | border-radius: 0.25rem; 471 | padding: 0.125rem 0.25rem; 472 | transition: all 0.2s ease; 473 | } 474 | 475 | .foundPageLink:hover { 476 | background-color: var(--emphasis-200); 477 | text-decoration: none; 478 | color: var(--text-color); 479 | border-bottom: 2px solid var(--primary-dark); 480 | } 481 | 482 | @keyframes slideDown { 483 | from { 484 | opacity: 0; 485 | transform: translateY(-10px); 486 | } 487 | 488 | to { 489 | opacity: 1; 490 | transform: translateY(0); 491 | } 492 | } 493 | 494 | @keyframes shimmer { 495 | from { 496 | background-position: 200% 0; 497 | } 498 | 499 | to { 500 | background-position: -200% 0; 501 | } 502 | } 503 | 504 | /* Chunked content styles for optimized streaming */ 505 | .chunkedContent { 506 | line-height: 1.6; 507 | /* Performance optimizations */ 508 | contain: layout style; 509 | transform: translateZ(0); 510 | /* Force hardware acceleration */ 511 | } 512 | 513 | .markdownChunk { 514 | /* Individual chunks that are cached and don't re-render */ 515 | contain: layout style paint; 516 | } 517 | 518 | .pendingChunk { 519 | /* Style for the currently streaming incomplete chunk */ 520 | font-family: var(--font-family-base); 521 | line-height: 1.6; 522 | color: var(--text-color); 523 | opacity: 0.9; 524 | } 525 | 526 | /* Welcome Screen Styles */ 527 | .welcomeScreen { 528 | display: flex; 529 | flex-direction: column; 530 | align-items: center; 531 | justify-content: center; 532 | height: 80vh; 533 | text-align: center; 534 | gap: 1.5rem; 535 | padding: 2rem; 536 | } 537 | 538 | .logoContainer { 539 | display: flex; 540 | align-items: center; 541 | justify-content: center; 542 | gap: 1rem; 543 | } 544 | 545 | .logoCircle { 546 | display: flex; 547 | align-items: center; 548 | justify-content: center; 549 | width: 5rem; 550 | height: 5rem; 551 | border-radius: 50%; 552 | } 553 | 554 | .groqLogo { 555 | background: var(--primary-color); 556 | } 557 | 558 | .cloudflareLogo { 559 | background: white; 560 | margin-left: -2rem; 561 | border: 1px solid var(--emphasis-200); 562 | } 563 | 564 | .logoImage { 565 | width: 2.5rem; 566 | height: 2.5rem; 567 | border-radius: 50%; 568 | } 569 | 570 | .welcomeContent { 571 | display: flex; 572 | flex-direction: column; 573 | gap: 0.75rem; 574 | } 575 | 576 | .welcomeTitle { 577 | font-size: 1.875rem; 578 | font-weight: bold; 579 | color: var(--text-color); 580 | } 581 | 582 | .welcomeDescription { 583 | color: var(--text-secondary); 584 | max-width: 42rem; 585 | line-height: 1.6; 586 | text-wrap: balance; 587 | } 588 | 589 | .welcomePrompt { 590 | font-size: 0.875rem; 591 | color: var(--text-secondary); 592 | opacity: 0.8; 593 | } --------------------------------------------------------------------------------