├── .node-version ├── agent ├── chatbot │ ├── __init__.py │ ├── db.py │ ├── agent.py │ ├── data.py │ └── server.py ├── .gitignore ├── README.md └── pyproject.toml ├── .npmrc ├── src ├── vite-env.d.ts ├── types.ts ├── lib │ ├── utils.ts │ └── tool-icons.tsx ├── main.tsx ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── separator.tsx │ │ ├── collapsible.tsx │ │ ├── textarea.tsx │ │ ├── input.tsx │ │ ├── avatar.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── checkbox.tsx │ │ ├── hover-card.tsx │ │ ├── badge.tsx │ │ ├── scroll-area.tsx │ │ ├── tooltip.tsx │ │ ├── button.tsx │ │ ├── sheet.tsx │ │ ├── dialog.tsx │ │ ├── carousel.tsx │ │ ├── select.tsx │ │ ├── dropdown-menu.tsx │ │ └── sidebar.tsx │ ├── ai-elements │ │ ├── image.tsx │ │ ├── response.tsx │ │ ├── suggestion.tsx │ │ ├── actions.tsx │ │ ├── message.tsx │ │ ├── conversation.tsx │ │ ├── loader.tsx │ │ ├── sources.tsx │ │ ├── task.tsx │ │ ├── tool.tsx │ │ ├── code-block.tsx │ │ ├── reasoning.tsx │ │ ├── branch.tsx │ │ ├── prompt-input.tsx │ │ ├── inline-citation.tsx │ │ └── web-preview.tsx │ ├── theme-provider.tsx │ ├── mode-toggle.tsx │ └── app-sidebar.tsx ├── hooks │ ├── use-mobile.ts │ └── useConversationIdFromUrl.tsx ├── assets │ └── logo.svg ├── App.tsx ├── Part.tsx ├── index.css └── Chat.tsx ├── .prettierignore ├── public ├── favicon.ico ├── favicon.png ├── apple-touch-icon.png └── favicon.svg ├── .prettierrc.json ├── .zed └── settings.json ├── specs ├── update-color-scheme.md ├── update-tools-dropdown.md └── delete-past-chats.md ├── README.md ├── .gitignore ├── components.json ├── index.html ├── tsconfig.json ├── vite.config.ts ├── .pre-commit-config.yaml ├── eslint.config.ts ├── package.json └── CLAUDE.md /.node-version: -------------------------------------------------------------------------------- 1 | lts/latest 2 | -------------------------------------------------------------------------------- /agent/chatbot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | ignore-workspace=true 3 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | .dist/ 3 | node_modules/ 4 | __pycache__/ 5 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydantic/ai-chat-ui/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydantic/ai-chat-ui/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pydantic/ai-chat-ui/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ConversationEntry { 2 | id: string 3 | firstMessage?: string 4 | timestamp: number 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "all", 5 | "tabWidth": 2, 6 | "printWidth": 119, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /agent/.gitignore: -------------------------------------------------------------------------------- 1 | typings/ 2 | 3 | # Python-generated files 4 | __pycache__/ 5 | *.py[oc] 6 | build/ 7 | dist/ 8 | wheels/ 9 | *.egg-info 10 | 11 | # Virtual environments 12 | .venv 13 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /.zed/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "languages": { 3 | "TSX": { 4 | "formatter": { 5 | "external": { 6 | "command": "npx", 7 | "arguments": ["prettier", "--stdin-filepath", "{buffer_path}"] 8 | } 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { cn } from '@/lib/utils' 3 | 4 | function Skeleton({ className, ...props }: React.ComponentProps<'div'>) { 5 | return
6 | } 7 | 8 | export { Skeleton } 9 | -------------------------------------------------------------------------------- /specs/update-color-scheme.md: -------------------------------------------------------------------------------- 1 | # Apply branded colors to the UI 2 | 3 | - Try to use `#04bd88` as a primary color in both dark and light mode. 4 | 5 | ``` 6 | // ignore for now, 7 | // - Try `rgb(119 255 216)` as a primary color in dark mode. 8 | ``` 9 | 10 | ## Implementation Details 11 | 12 | Convert the colors using makeshift node scripts. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pydantic AI Chat package 2 | 3 | Example React frontend for Pydantic AI Chat using [Vercel AI Elements](https://vercel.com/changelog/introducing-ai-elements). 4 | 5 | ## Dev 6 | 7 | ```sh 8 | npm install 9 | npm run dev 10 | 11 | # stop your logfire platform, to avoid port 8000 conflicts 12 | 13 | cd agent && uv run uvicorn chatbot.server:app 14 | ``` 15 | -------------------------------------------------------------------------------- /.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 | /.claude/settings.local.json 26 | /scratch/ 27 | /agent/.venv/ 28 | /tsconfig.tsbuildinfo 29 | .env 30 | *.tgz 31 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": {} 22 | } 23 | -------------------------------------------------------------------------------- /specs/update-tools-dropdown.md: -------------------------------------------------------------------------------- 1 | # Improve the tools dropdown 2 | 3 | - Replace the trigger with a button that uses the lucide icon for settings, add tooltip for tools. Use shadcn for that. 4 | - Hard-code some icons for known tools. These are the tools we know right now. Pick some icons from lucide. 5 | 6 | ```ts 7 | ;['web_search', 'code_execution', 'image_generation'] 8 | ``` 9 | 10 | For other tools, use the default wrench icon. 11 | 12 | - Change the dropdown from checkboxes to icons on the left, and switch component on the right, use shadcn for that. 13 | -------------------------------------------------------------------------------- /src/components/ai-elements/image.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import { cn } from '@/lib/utils' 3 | import type { Experimental_GeneratedImage } from 'ai' 4 | 5 | export type ImageProps = Experimental_GeneratedImage & { 6 | className?: string 7 | alt?: string 8 | } 9 | 10 | export const Image = ({ base64, uint8Array, mediaType, ...props }: ImageProps) => ( 11 | {props.alt} 17 | ) 18 | -------------------------------------------------------------------------------- /src/lib/tool-icons.tsx: -------------------------------------------------------------------------------- 1 | import { CodeIcon, DownloadIcon, GlobeIcon, ImagePlusIcon, WrenchIcon } from 'lucide-react' 2 | import type { ReactNode } from 'react' 3 | 4 | export function getToolIcon(toolId: string, className = 'size-4') { 5 | const iconMap: Record = { 6 | web_search: , 7 | web_fetch: , 8 | code_execution: , 9 | image_generation: , 10 | } 11 | return iconMap[toolId] ?? 12 | } 13 | -------------------------------------------------------------------------------- /src/components/ai-elements/response.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils' 2 | import { type ComponentProps, memo } from 'react' 3 | import { Streamdown } from 'streamdown' 4 | 5 | type ResponseProps = ComponentProps 6 | 7 | export const Response = memo( 8 | ({ className, ...props }: ResponseProps) => ( 9 | *:first-child]:mt-0 [&>*:last-child]:mb-0 code-bg ', className)} 11 | {...props} 12 | /> 13 | ), 14 | (prevProps, nextProps) => prevProps.children === nextProps.children, 15 | ) 16 | 17 | Response.displayName = 'Response' 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Pydantic AI 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /agent/README.md: -------------------------------------------------------------------------------- 1 | # Logfire Docs chatbot 2 | 3 | ## Usage 4 | 5 | Make sure you have `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY` set in your environment variables. 6 | 7 | Run the agent backend + pre-packaged frontend: 8 | 9 | ```bash 10 | cd agent 11 | uv sync 12 | uv run uvicorn chatbot.server:app 13 | ``` 14 | 15 | Then open your browser to `http://localhost:8000`. 16 | 17 | ### Frontend development 18 | 19 | Optionally run the frontend in dev mode, which will connect to the running agent backend: 20 | 21 | ```bash 22 | npm install 23 | npm run dev 24 | ``` 25 | 26 | Then open your browser to `http://localhost:5173`. -------------------------------------------------------------------------------- /src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener('change', onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => { 16 | mql.removeEventListener('change', onChange) 17 | } 18 | }, []) 19 | 20 | return !!isMobile 21 | } 22 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /specs/delete-past-chats.md: -------------------------------------------------------------------------------- 1 | # Add the ability to delete past chats 2 | 3 | ## Feature Description 4 | 5 | In the side panel, a list of previous chats is displayed. Each chat entry includes a delete button (represented by a trash can icon) next to it. When the user clicks the delete button, a confirmation dialog appears asking, "Are you sure you want to delete this chat?" with "Cancel" and "Delete" options. If the user confirms the deletion, the selected chat is removed from the list and any associated data is deleted from the local storage. If the user cancels, no action is taken and the dialog closes. 6 | 7 | ## Implementation Details 8 | 9 | - The chats are stored in local storage, just remove them from there. 10 | - Use shadcn/lucide for the UI. The button should be visible only on hover. 11 | -------------------------------------------------------------------------------- /agent/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "logfire-docs-chatbot" 3 | version = "0.1.0" 4 | requires-python = ">=3.12" 5 | dependencies = [ 6 | "bs4>=0.0.2", 7 | "fastapi>=0.117.1", 8 | "lancedb>=0.25.0", 9 | "langchain-text-splitters>=0.3.11", 10 | "logfire[fastapi]>=4.9.0", 11 | "markdown2>=2.5.4", 12 | "pip>=25.2", 13 | "pydantic-ai-slim[anthropic,openai,google,cli]>=1.14.0", 14 | "pyright>=1.1.405", 15 | "python-frontmatter>=1.1.0", 16 | "sentence-transformers>=5.1.1", 17 | "sse-starlette>=3.0.2", 18 | "starlette>=0.48.0", 19 | "uvicorn>=0.37.0", 20 | "watchfiles>=1.1.0", 21 | ] 22 | 23 | [tool.ruff.format] 24 | # don't format python in docstrings, pytest-examples takes care of it 25 | docstring-code-format = false 26 | quote-style = "single" 27 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as SeparatorPrimitive from '@radix-ui/react-separator' 3 | 4 | import { cn } from '@/lib/utils' 5 | 6 | function Separator({ 7 | className, 8 | orientation = 'horizontal', 9 | decorative = true, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 23 | ) 24 | } 25 | 26 | export { Separator } 27 | -------------------------------------------------------------------------------- /src/components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as CollapsiblePrimitive from '@radix-ui/react-collapsible' 3 | 4 | function Collapsible({ ...props }: React.ComponentProps) { 5 | return 6 | } 7 | 8 | function CollapsibleTrigger({ ...props }: React.ComponentProps) { 9 | return 10 | } 11 | 12 | function CollapsibleContent({ ...props }: React.ComponentProps) { 13 | return 14 | } 15 | 16 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 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 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "erasableSyntaxOnly": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true, 24 | "paths": { 25 | "@/*": ["./src/*"] 26 | } 27 | }, 28 | "include": ["src", "eslint.config.ts", "vite.config.ts"] 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { 6 | return ( 7 |