├── 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 |
9 |
10 |
11 |
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 |
71 | {copied ? : }
72 |
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 |
184 | )}
185 |
186 |
187 |
188 |
189 |
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 | }
--------------------------------------------------------------------------------