├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md ├── chat-stats.png ├── eslint.config.js ├── index.html ├── model-playground.png ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── qwen.webp ├── smollm.png └── vite.svg ├── src ├── App.tsx ├── assets │ ├── meta.svg │ ├── qwen.svg │ └── react.svg ├── components │ ├── ChatBox.tsx │ ├── ChatHeader.tsx │ ├── ChatStats.tsx │ ├── Header.tsx │ ├── InputArea.tsx │ ├── ModelSearch │ │ ├── FamilyFilter.tsx │ │ ├── ModelList.tsx │ │ ├── ModelRow.tsx │ │ ├── ModelSearch.tsx │ │ └── SearchInput.tsx │ ├── ModelSelector.tsx │ └── icons │ │ ├── GoogleIcon.tsx │ │ ├── MetaIcon.tsx │ │ ├── MicrosoftIcon.tsx │ │ ├── MistralIcon.tsx │ │ ├── QwenIcon.tsx │ │ ├── SmolLMIcon.tsx │ │ ├── SnowflakeIcon.tsx │ │ └── index.ts ├── index.css ├── main.tsx └── utils │ └── llm.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.app.tsbuildinfo ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.node.tsbuildinfo ├── vite.config.ts └── webllm.gif /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Hugging Face Spaces 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-and-deploy: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: '20' 21 | 22 | - name: Install dependencies 23 | run: npm ci 24 | 25 | - name: Build project 26 | run: npm run build 27 | 28 | - name: Copy README to dist 29 | run: cp README.md dist/ 30 | 31 | - name: Push to Hugging Face Spaces 32 | env: 33 | HF_TOKEN: ${{ secrets.HF_TOKEN }} 34 | run: | 35 | cd dist 36 | git init 37 | git config user.email "github-actions[bot]@users.noreply.github.com" 38 | git config user.name "github-actions[bot]" 39 | git add . 40 | git commit -m "Update build artifacts and README" 41 | git push --force https://${{ secrets.HF_USERNAME }}:$HF_TOKEN@huggingface.co/spaces/${{ secrets.HF_USERNAME }}/${{ secrets.SPACE_NAME }} HEAD:main -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: WebLLM Playground 3 | emoji: 🏎️ 4 | colorFrom: purple 5 | colorTo: indigo 6 | sdk: static 7 | pinned: true 8 | header: mini 9 | license: apache-2.0 10 | --- 11 | ![WebLLM Playground](https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/webllm/WebLLM.png) 12 | 13 |
14 | 15 | **WebLLM Playground** is built on top of [MLC-LLM](https://github.com/mlc-ai/mlc-llm) and [WebLLM Chat](https://github.com/mlc-ai/web-llm-chat). 16 | 17 | [![NPM Package](https://img.shields.io/badge/NPM_Package-Published-cc3534)](https://www.npmjs.com/package/@mlc-ai/web-llm) 18 | [!["WebLLM Chat Deployed"](https://img.shields.io/badge/WebLLM_Chat-Deployed-%2332a852)](https://chat.webllm.ai/) 19 | [![Join Discoard](https://img.shields.io/badge/Join-Discord-7289DA?logo=discord&logoColor=white)]("https://discord.gg/9Xpy2HGBuD") 20 | [![Related Repository: WebLLM Chat](https://img.shields.io/badge/Related_Repo-WebLLM_Chat-fafbfc?logo=github)](https://github.com/mlc-ai/web-llm-chat/) 21 | [![Related Repository: MLC LLM](https://img.shields.io/badge/Related_Repo-MLC_LLM-fafbfc?logo=github)](https://github.com/mlc-ai/mlc-llm/) 22 | --- 23 | 24 | 25 |
26 | 27 | ![WebLLM Gif](./webllm.gif) 28 | 29 | ## Chat Stats 30 | 31 | ![Chat Stats](./chat-stats.png) 32 | 33 | ## Model Selector 34 | 35 | ![Model Selector](./model-playground.png) 36 | -------------------------------------------------------------------------------- /chat-stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfahlgren1/webllm-playground/1a61c58f69413f84126b2443bba5fc3ef75c869b/chat-stats.png -------------------------------------------------------------------------------- /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 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /model-playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfahlgren1/webllm-playground/1a61c58f69413f84126b2443bba5fc3ef75c869b/model-playground.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webllm-chat-react", 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": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@fontsource/inter": "^5.1.0", 14 | "@mlc-ai/web-llm": "^0.2.71", 15 | "@tailwindcss/typography": "^0.5.15", 16 | "@types/react-syntax-highlighter": "^15.5.13", 17 | "lucide-react": "^0.446.0", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "react-syntax-highlighter": "^15.5.0", 21 | "rehype-highlight": "^7.0.0", 22 | "rehype-katex": "^7.0.1", 23 | "rehype-stringify": "^10.0.1", 24 | "remark-breaks": "^4.0.0", 25 | "remark-frontmatter": "^5.0.0", 26 | "remark-gfm": "^4.0.0", 27 | "remark-math": "^6.0.0", 28 | "remark-parse": "^11.0.0", 29 | "remark-rehype": "^11.1.1", 30 | "unified": "^11.0.5" 31 | }, 32 | "devDependencies": { 33 | "@eslint/js": "^9.9.0", 34 | "@types/react": "^18.3.3", 35 | "@types/react-dom": "^18.3.0", 36 | "@vitejs/plugin-react": "^4.3.1", 37 | "autoprefixer": "^10.4.20", 38 | "eslint": "^9.9.0", 39 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 40 | "eslint-plugin-react-refresh": "^0.4.9", 41 | "globals": "^15.9.0", 42 | "postcss": "^8.4.47", 43 | "tailwindcss": "^3.4.13", 44 | "typescript": "^5.5.3", 45 | "typescript-eslint": "^8.0.1", 46 | "vite": "^5.4.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/qwen.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfahlgren1/webllm-playground/1a61c58f69413f84126b2443bba5fc3ef75c869b/public/qwen.webp -------------------------------------------------------------------------------- /public/smollm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfahlgren1/webllm-playground/1a61c58f69413f84126b2443bba5fc3ef75c869b/public/smollm.png -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import Header from './components/Header'; 3 | import ModelSelector from './components/ModelSelector'; 4 | import ChatBox from './components/ChatBox'; 5 | import InputArea from "./components/InputArea"; 6 | import ChatStats from './components/ChatStats'; 7 | import { initializeWebLLMEngine, streamingGenerating, availableModels, setProgressCallback, modelDetailsList } from './utils/llm'; 8 | import '@fontsource/inter'; 9 | import ChatHeader from './components/ChatHeader'; 10 | 11 | const TEMPERATURE = 0.7; 12 | const TOP_P = 1; 13 | const PAGE_TITLE = "WebLLM Playground ✨"; 14 | const PAGE_HEADING = "WebLLM Playground ✨"; 15 | const PAGE_DESCRIPTION = "Blazing fast inference with WebGPU and WebLLM running locally in your browser."; 16 | 17 | // @ts-expect-error - navigator.gpu is not yet in TypeScript's lib 18 | const IS_WEB_GPU_ENABLED = !!navigator.gpu; 19 | 20 | interface Message { 21 | content: string; 22 | role: 'system' | 'user' | 'assistant'; 23 | } 24 | 25 | interface ChatStatistics { 26 | promptTokens: number; 27 | completionTokens: number; 28 | prefillSpeed: number; 29 | decodingSpeed: number; 30 | } 31 | 32 | function App() { 33 | const [isModelLoaded, setIsModelLoaded] = useState(false); 34 | const [selectedModel, setSelectedModel] = useState(availableModels[0]); 35 | const [chatMessages, setChatMessages] = useState([]); 36 | const [isGeneratingResponse, setIsGeneratingResponse] = useState(false); 37 | const [chatStatistics, setChatStatistics] = useState({ 38 | promptTokens: 0, 39 | completionTokens: 0, 40 | prefillSpeed: 0, 41 | decodingSpeed: 0, 42 | }); 43 | const [loadingProgress, setLoadingProgress] = useState(''); 44 | const [isModelSelectorVisible, setIsModelSelectorVisible] = useState(true); 45 | const [areChatStatsVisible, setAreChatStatsVisible] = useState(true); 46 | 47 | const downloadStatusRef = useRef(null); 48 | 49 | useEffect(() => { 50 | document.title = PAGE_TITLE; 51 | 52 | setProgressCallback((progress: string) => { 53 | setLoadingProgress(progress); 54 | }); 55 | }, []); 56 | 57 | const handleModelLoad = async (): Promise => { 58 | try { 59 | setLoadingProgress('Loading...'); 60 | await initializeWebLLMEngine( 61 | selectedModel, 62 | TEMPERATURE, 63 | TOP_P, 64 | () => { 65 | setIsModelLoaded(true); 66 | setLoadingProgress('Model loaded successfully'); 67 | setIsModelSelectorVisible(false); 68 | } 69 | ); 70 | } catch (error) { 71 | console.error("Error loading model:", error); 72 | setLoadingProgress('Error loading model. Please try again.'); 73 | } 74 | }; 75 | 76 | const handleSendMessage = async (input: string): Promise => { 77 | if (!isModelLoaded || input.trim().length === 0) return; 78 | 79 | setIsGeneratingResponse(true); 80 | const updatedMessages: Message[] = [...chatMessages, { content: input, role: "user" }]; 81 | setChatMessages(updatedMessages); 82 | 83 | try { 84 | await streamingGenerating( 85 | updatedMessages, 86 | (currentMessage: string) => { 87 | const newMessage: Message = { content: currentMessage, role: "assistant" }; 88 | setChatMessages([...updatedMessages, newMessage]); 89 | }, 90 | (finalMessage: string, usage: any) => { 91 | const finalAssistantMessage: Message = { content: finalMessage, role: "assistant" }; 92 | setChatMessages([...updatedMessages, finalAssistantMessage]); 93 | 94 | const updatedStats = { 95 | promptTokens: Math.round(usage.prompt_tokens) || 0, 96 | completionTokens: Math.round(usage.completion_tokens) || 0, 97 | prefillSpeed: Math.round(usage.extra?.prefill_tokens_per_s) || 0, 98 | decodingSpeed: Math.round(usage.extra?.decode_tokens_per_s) || 0, 99 | }; 100 | 101 | 102 | setChatStatistics(updatedStats); 103 | setIsGeneratingResponse(false); 104 | }, 105 | (error: Error) => { 106 | console.error("Error generating response:", error); 107 | setIsGeneratingResponse(false); 108 | } 109 | ); 110 | } catch (error) { 111 | console.error("Error in handleSendMessage:", error); 112 | setIsGeneratingResponse(false); 113 | } 114 | }; 115 | 116 | const handleExampleSelection = (message: string) => { 117 | handleSendMessage(message); 118 | }; 119 | 120 | const handleChatClear = () => { 121 | setChatMessages([]); 122 | }; 123 | 124 | // Retrieve the icon for the selected model 125 | let selectedModelDetails = null; 126 | for (let i = 0; i < modelDetailsList.length; i++) { 127 | if (selectedModel.toLowerCase().includes(modelDetailsList[i].name)) { 128 | selectedModelDetails = modelDetailsList[i]; 129 | break; 130 | } 131 | } 132 | const SelectedModelIcon = selectedModelDetails ? selectedModelDetails.icon : null; 133 | 134 | if (!IS_WEB_GPU_ENABLED) { 135 | return ( 136 |
137 |
138 |
139 |

140 | WebGPU is not supported in your browser. 😢 141 |

142 | 153 |
154 |
155 | ); 156 | } 157 | 158 | return ( 159 |
160 | {/* Chat Header */} 161 | {isModelLoaded && !isModelSelectorVisible && ( 162 |
163 | 168 |
169 | )} 170 | 171 | {/* Model Selector */} 172 | {isModelSelectorVisible && ( 173 |
174 |
175 | 183 |
184 |
185 | )} 186 | 187 | {/* Chat Area */} 188 | {isModelLoaded && ( 189 |
190 |
191 | 196 |
197 |
198 | )} 199 | 200 | {/* Input Area */} 201 | {isModelLoaded && ( 202 |
203 | 208 |
209 | )} 210 | 211 | {/* Chat Statistics */} 212 | {areChatStatsVisible && ( 213 | setAreChatStatsVisible(false)} 216 | /> 217 | )} 218 |
219 | ); 220 | } 221 | 222 | export default App; -------------------------------------------------------------------------------- /src/assets/meta.svg: -------------------------------------------------------------------------------- 1 | facebook-meta -------------------------------------------------------------------------------- /src/assets/qwen.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfahlgren1/webllm-playground/1a61c58f69413f84126b2443bba5fc3ef75c869b/src/assets/qwen.svg -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ChatBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from 'react'; 2 | import rehypeStringify from "rehype-stringify"; 3 | import remarkFrontmatter from "remark-frontmatter"; 4 | import remarkGfm from "remark-gfm"; 5 | import RemarkBreaks from "remark-breaks"; 6 | import remarkParse from "remark-parse"; 7 | import remarkRehype from "remark-rehype"; 8 | import RehypeKatex from "rehype-katex"; 9 | import { unified } from "unified"; 10 | import remarkMath from "remark-math"; 11 | import rehypeHighlight from "rehype-highlight"; 12 | 13 | import 'highlight.js/styles/dracula.css'; 14 | 15 | interface ChatBoxProps { 16 | messages: { content: string; role: 'system' | 'user' | 'assistant' }[]; 17 | onExampleClick: (message: string) => void; 18 | isGenerating: boolean; 19 | } 20 | 21 | const ChatBox: React.FC = ({ messages, onExampleClick, isGenerating }) => { 22 | const chatBoxRef = useRef(null); 23 | const bottomRef = useRef(null); 24 | 25 | const scrollToBottom = () => { 26 | if (bottomRef.current) { 27 | bottomRef.current.scrollIntoView({ behavior: 'smooth' }); 28 | } 29 | }; 30 | 31 | useEffect(() => { 32 | if (isGenerating) { 33 | const scrollInterval = setInterval(scrollToBottom, 100); 34 | return () => clearInterval(scrollInterval); 35 | } 36 | }, [isGenerating]); 37 | 38 | useEffect(() => { 39 | if (messages.length > 0 && messages[messages.length - 1].role === 'user') { 40 | scrollToBottom(); 41 | } 42 | }, [messages]); 43 | 44 | const exampleMessages = [ 45 | "Show me the code for a simple web app", 46 | "Implement fib(n) in Python", 47 | "What is refraction?", 48 | "Explain thermal conductivity" 49 | ]; 50 | 51 | const messageFormatter = unified() 52 | .use(remarkParse) 53 | .use(remarkFrontmatter) 54 | .use(remarkMath) 55 | .use(remarkGfm) 56 | .use(RemarkBreaks) 57 | .use(remarkRehype) 58 | .use(rehypeStringify) 59 | .use(RehypeKatex) 60 | .use(rehypeHighlight); 61 | 62 | const renderMarkdown = (content: string) => { 63 | const processedContent = messageFormatter.processSync(content); 64 | 65 | return
; 66 | }; 67 | 68 | const renderExampleButtons = () => { 69 | const buttons = []; 70 | for (let index = 0; index < exampleMessages.length; index++) { 71 | const message = exampleMessages[index]; 72 | buttons.push( 73 | 80 | ); 81 | } 82 | return buttons; 83 | }; 84 | 85 | const renderMessages = () => { 86 | const messageElements = []; 87 | for (const message of messages) { 88 | messageElements.push( 89 |
97 |
98 | {renderMarkdown(message.content)} 99 |
100 |
101 | ); 102 | } 103 | return messageElements; 104 | }; 105 | 106 | return ( 107 |
108 |
112 | {messages.length === 0 ? ( 113 |
114 | {renderExampleButtons()} 115 |
116 | ) : ( 117 |
118 | {renderMessages()} 119 |
120 |
121 | )} 122 |
123 |
124 | ); 125 | }; 126 | 127 | export default ChatBox; -------------------------------------------------------------------------------- /src/components/ChatHeader.tsx: -------------------------------------------------------------------------------- 1 | import React, { ComponentType } from 'react'; 2 | 3 | interface ChatHeaderProps { 4 | selectedModel: string; 5 | SelectedModelIcon: ComponentType> | null; 6 | onClearChat: () => void; 7 | } 8 | 9 | const ChatHeader: React.FC = ({ selectedModel, SelectedModelIcon, onClearChat }) => { 10 | return ( 11 |
12 |
13 | {SelectedModelIcon && } 14 | {selectedModel} 15 |
16 | 25 |
26 | ); 27 | }; 28 | 29 | export default ChatHeader; 30 | -------------------------------------------------------------------------------- /src/components/ChatStats.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | 3 | interface ChatStatsProps { 4 | stats: Usage; 5 | onClose: () => void; 6 | } 7 | 8 | export interface Usage { 9 | promptTokens: number; 10 | completionTokens: number; 11 | prefillSpeed: number; 12 | decodingSpeed: number; 13 | } 14 | 15 | 16 | const ChatStats: React.FC = ({ stats, onClose }) => { 17 | const [isDragging, setIsDragging] = useState(false); 18 | const [position, setPosition] = useState({ x: 16, y: 100 }); 19 | const statsRef = useRef(null); 20 | const offsetRef = useRef({ x: 0, y: 0 }); 21 | 22 | const shouldShowStats = Object.values(stats).some(value => value !== 0); 23 | 24 | useEffect(() => { 25 | const handleMouseMove = (e: MouseEvent) => { 26 | if (isDragging) { 27 | const newX = e.clientX - offsetRef.current.x; 28 | const newY = e.clientY - offsetRef.current.y; 29 | setPosition({ x: newX, y: newY }); 30 | } 31 | }; 32 | 33 | const handleMouseUp = () => { 34 | setIsDragging(false); 35 | }; 36 | 37 | document.addEventListener('mousemove', handleMouseMove); 38 | document.addEventListener('mouseup', handleMouseUp); 39 | 40 | return () => { 41 | document.removeEventListener('mousemove', handleMouseMove); 42 | document.removeEventListener('mouseup', handleMouseUp); 43 | }; 44 | }, [isDragging]); 45 | 46 | const handleMouseDown = (e: React.MouseEvent) => { 47 | if (statsRef.current) { 48 | const rect = statsRef.current.getBoundingClientRect(); 49 | offsetRef.current = { 50 | x: e.clientX - rect.left, 51 | y: e.clientY - rect.top 52 | }; 53 | setIsDragging(true); 54 | } 55 | }; 56 | 57 | if (!shouldShowStats) { 58 | return null; 59 | } 60 | 61 | return ( 62 |
67 |
71 | Chat Stats 72 | 78 |
79 |
80 |
81 |
82 |
Prompt:
83 |
{Math.round(stats.promptTokens || 0)}
84 |
85 |
86 |
Completion:
87 |
{Math.round(stats.completionTokens || 0)}
88 |
89 |
90 |
Total:
91 |
{Math.round((stats.promptTokens || 0) + (stats.completionTokens || 0))}
92 |
93 |
94 |
Prefill:
95 |
{Math.round(stats.prefillSpeed || 0)} tok/s
96 |
97 |
98 |
Decoding:
99 |
{Math.round(stats.decodingSpeed || 0)} tok/s
100 |
101 |
102 |
103 |
104 | ); 105 | }; 106 | 107 | export default ChatStats; -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '@fontsource/inter'; 3 | import { Github, Star } from 'lucide-react'; 4 | 5 | interface HeaderProps { 6 | heading: string; 7 | description: string; 8 | } 9 | 10 | const Header: React.FC = ({ heading, description }) => { 11 | return ( 12 |
13 |

14 | {heading} 15 |

16 |

17 | {description} 18 |

19 | 32 |
33 | ); 34 | }; 35 | 36 | export default Header; -------------------------------------------------------------------------------- /src/components/InputArea.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect } from 'react'; 2 | 3 | interface InputAreaProps { 4 | onSendMessage: (input: string) => Promise; 5 | isGenerating: boolean; 6 | isModelLoaded: boolean; 7 | } 8 | 9 | const InputArea: React.FC = ({ onSendMessage, isGenerating, isModelLoaded }) => { 10 | const [input, setInput] = useState(''); 11 | const inputRef = useRef(null); 12 | 13 | const handleSend = () => { 14 | if (input.trim() && !isGenerating && isModelLoaded) { 15 | onSendMessage(input); 16 | setInput(''); 17 | } 18 | }; 19 | 20 | useEffect(() => { 21 | if (!isGenerating && inputRef.current) { 22 | inputRef.current.focus(); 23 | } 24 | }, [isGenerating]); 25 | 26 | const isSendDisabled = !isModelLoaded || isGenerating || !input.trim(); 27 | 28 | return ( 29 |
30 |
31 | setInput(e.target.value)} 35 | placeholder={isModelLoaded ? "Type a message..." : "Select and load a model from the menu to start."} 36 | disabled={!isModelLoaded || isGenerating} 37 | className="flex-grow p-3 rounded-lg bg-[var(--bg-color)] focus:outline-none focus:ring-2 focus:[var(--border-color)] text-gray-300 disabled:opacity-50 disabled:cursor-not-allowed" 38 | onKeyPress={(e) => e.key === 'Enter' && !isSendDisabled && handleSend()} 39 | ref={inputRef} 40 | /> 41 | 59 |
60 |
61 | ); 62 | }; 63 | 64 | export default InputArea; -------------------------------------------------------------------------------- /src/components/ModelSearch/FamilyFilter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface FamilyFilterProps { 4 | sortedModelFamilies: [string, { name: string; icon: React.ComponentType> }][]; 5 | selectedFamilies: string[]; 6 | onToggleFamilyFilter: (family: string) => void; 7 | } 8 | 9 | const FamilyFilter: React.FC = ({ 10 | sortedModelFamilies, 11 | selectedFamilies, 12 | onToggleFamilyFilter, 13 | }) => { 14 | return ( 15 |
16 | {sortedModelFamilies.map(([key, { name, icon: Icon }]) => ( 17 | 29 | ))} 30 |
31 | ); 32 | }; 33 | 34 | export default FamilyFilter; 35 | -------------------------------------------------------------------------------- /src/components/ModelSearch/ModelList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface ModelListProps { 4 | filteredModels: [string, string[]][]; 5 | renderModelRow: (model: [string, string[]], index: number) => React.ReactNode; 6 | } 7 | 8 | const ModelList: React.FC = ({ filteredModels, renderModelRow }) => { 9 | const columns: React.ReactNode[][] = [[], [], []]; 10 | 11 | for (const [index, model] of filteredModels.entries()) { 12 | const columnIndex = index % 3; 13 | columns[columnIndex].push(renderModelRow(model, index)); 14 | } 15 | 16 | return ( 17 |
18 |
19 | {columns.map((column, index) => ( 20 |
21 | {column} 22 |
23 | ))} 24 |
25 |
26 | ); 27 | }; 28 | 29 | export default ModelList; -------------------------------------------------------------------------------- /src/components/ModelSearch/ModelRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChevronDown, ChevronUp } from 'lucide-react'; 3 | 4 | interface ModelRowProps { 5 | baseModel: string; 6 | variants: string[]; 7 | isExpanded: boolean; 8 | hasSingleVariant: boolean; 9 | determineModelIcon: (model: string) => JSX.Element; 10 | extractModelDetails: (model: string) => { displayName: string; quantBadge: string | null }; 11 | onSelectModel: (model: string) => void; 12 | onClose: () => void; 13 | handleToggleExpand: (modelName: string) => void; 14 | } 15 | 16 | const ModelRow: React.FC = ({ 17 | baseModel, 18 | variants, 19 | isExpanded, 20 | hasSingleVariant, 21 | determineModelIcon, 22 | extractModelDetails, 23 | onSelectModel, 24 | onClose, 25 | handleToggleExpand, 26 | }) => { 27 | const { quantBadge } = hasSingleVariant ? extractModelDetails(variants[0]) : { quantBadge: null }; 28 | 29 | return ( 30 |
31 |
{ 34 | if (hasSingleVariant) { 35 | onSelectModel(variants[0]); 36 | onClose(); 37 | } else { 38 | handleToggleExpand(baseModel); 39 | } 40 | }} 41 | > 42 |
43 |
44 | {determineModelIcon(baseModel)} 45 | 46 | {baseModel} 47 | 48 | {hasSingleVariant && quantBadge && ( 49 | 50 | {quantBadge} 51 | 52 | )} 53 |
54 | {!hasSingleVariant && ( 55 | isExpanded ? : 56 | )} 57 |
58 |
59 | {isExpanded && !hasSingleVariant && ( 60 |
61 | {variants.map(variant => { 62 | const { quantBadge } = extractModelDetails(variant); 63 | return ( 64 |
{ 67 | onSelectModel(variant); 68 | onClose(); 69 | }} 70 | className="flex items-center justify-between p-2 rounded-md hover:bg-gray-700 cursor-pointer" 71 | > 72 | {quantBadge || variant} 73 | 74 |
75 | ); 76 | })} 77 |
78 | )} 79 |
80 | ); 81 | }; 82 | 83 | export default ModelRow; 84 | -------------------------------------------------------------------------------- /src/components/ModelSearch/ModelSearch.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback, useRef } from 'react'; 2 | import { X, Cpu } from 'lucide-react'; 3 | import SearchInput from './SearchInput'; 4 | import FamilyFilter from './FamilyFilter'; 5 | import ModelList from './ModelList'; 6 | import ModelRow from './ModelRow'; 7 | import { modelDetailsList } from '../../utils/llm'; 8 | 9 | export interface ModelSearchProps { 10 | isOpen: boolean; 11 | onClose: () => void; 12 | availableModels: string[]; 13 | onSelectModel: (model: string) => void; 14 | } 15 | 16 | const modelFamilies: { [key: string]: { name: string; icon: React.ComponentType> } } = {}; 17 | 18 | for (const modelDetail of modelDetailsList) { 19 | modelFamilies[modelDetail.name] = { 20 | name: modelDetail.name.charAt(0).toUpperCase() + modelDetail.name.slice(1), 21 | icon: modelDetail.icon 22 | }; 23 | } 24 | 25 | const ModelSearch: React.FC = ({ 26 | isOpen, 27 | onClose, 28 | availableModels, 29 | onSelectModel, 30 | }) => { 31 | const [searchTerm, setSearchTerm] = useState(''); 32 | const [filteredModels, setFilteredModels] = useState<[string, string[]][]>([]); 33 | const [selectedFamilies, setSelectedFamilies] = useState([]); 34 | const [expandedModels, setExpandedModels] = useState>(new Set()); 35 | 36 | const determineModelIcon = (model: string) => { 37 | const modelDetail = modelDetailsList.find(md => model.toLowerCase().includes(md.name)); 38 | return modelDetail ? 39 | : ; 40 | }; 41 | 42 | const identifyModelFamily = (model: string): string | null => { 43 | return modelDetailsList.find(md => model.toLowerCase().includes(md.name))?.name || null; 44 | }; 45 | 46 | const extractModelDetails = (model: string) => { 47 | const parts = model.split('-'); 48 | const displayName: string[] = []; 49 | const quantBadges: string[] = []; 50 | let isBadge = false; 51 | 52 | for (let i = 0; i < parts.length; i++) { 53 | const part = parts[i]; 54 | if (isBadge || part.startsWith('q') || part.startsWith('b')) { 55 | isBadge = true; 56 | if (part !== 'MLC') { 57 | quantBadges.push(part); 58 | } 59 | } else { 60 | displayName.push(part); 61 | } 62 | } 63 | 64 | return { 65 | displayName: displayName.join(' '), 66 | quantBadge: quantBadges.length > 0 ? quantBadges.join('-') : null, 67 | }; 68 | }; 69 | 70 | const sortAndGroupModels = useCallback((models: string[]): [string, string[]][] => { 71 | const groupedModels: { [key: string]: string[] } = {}; 72 | 73 | for (const model of models) { 74 | const { displayName } = extractModelDetails(model); 75 | const family = identifyModelFamily(model); 76 | 77 | if (family) { 78 | if (!groupedModels[displayName]) { 79 | groupedModels[displayName] = []; 80 | } 81 | groupedModels[displayName].push(model); 82 | } 83 | } 84 | 85 | for (const key in groupedModels) { 86 | groupedModels[key].sort((a, b) => a.localeCompare(b)); 87 | } 88 | 89 | return Object.entries(groupedModels).sort(([, aVariants], [, bVariants]) => { 90 | const familyA = identifyModelFamily(aVariants[0]) || ''; 91 | const familyB = identifyModelFamily(bVariants[0]) || ''; 92 | return familyA.localeCompare(familyB); 93 | }); 94 | }, []); 95 | 96 | const handleToggleExpand = (modelName: string) => { 97 | setExpandedModels(prev => { 98 | const updatedSet = new Set(prev); 99 | if (updatedSet.has(modelName)) { 100 | updatedSet.delete(modelName); 101 | } else { 102 | updatedSet.add(modelName); 103 | } 104 | return updatedSet; 105 | }); 106 | }; 107 | 108 | const resetState = useCallback(() => { 109 | setSearchTerm(''); 110 | setSelectedFamilies([]); 111 | setExpandedModels(new Set()); 112 | }, []); 113 | 114 | const handleClose = useCallback(() => { 115 | resetState(); 116 | onClose(); 117 | }, [onClose, resetState]); 118 | 119 | const handleOverlayClick = (event: React.MouseEvent) => { 120 | if (event.target === event.currentTarget) { 121 | handleClose(); 122 | } 123 | }; 124 | 125 | useEffect(() => { 126 | const sortedModels = sortAndGroupModels(availableModels); 127 | 128 | let filtered = sortedModels; 129 | 130 | if (searchTerm) { 131 | const lowerSearchTerm = searchTerm.toLowerCase(); 132 | filtered = sortedModels.filter(([baseModel, variants]) => 133 | baseModel.toLowerCase().includes(lowerSearchTerm) || 134 | variants.some(v => v.toLowerCase().includes(lowerSearchTerm)) 135 | ); 136 | } 137 | 138 | if (selectedFamilies.length > 0) { 139 | filtered = filtered.filter(([, variants]) => { 140 | const family = identifyModelFamily(variants[0]); 141 | return family && selectedFamilies.includes(family); 142 | }); 143 | } 144 | 145 | setFilteredModels(filtered); 146 | }, [searchTerm, availableModels, selectedFamilies, sortAndGroupModels]); 147 | 148 | const handleToggleFamilyFilter = (family: string) => { 149 | setSelectedFamilies(prev => 150 | prev.includes(family) 151 | ? prev.filter(f => f !== family) 152 | : [...prev, family] 153 | ); 154 | }; 155 | 156 | const searchInputRef = useRef(null); 157 | 158 | useEffect(() => { 159 | if (isOpen) { 160 | document.body.style.overflow = 'hidden'; 161 | // Focus on the search input when the modal opens 162 | setTimeout(() => { 163 | searchInputRef.current?.focus(); 164 | }, 0); 165 | } else { 166 | document.body.style.overflow = ''; 167 | resetState(); 168 | } 169 | 170 | return () => { 171 | document.body.style.overflow = ''; 172 | }; 173 | }, [isOpen, resetState]); 174 | 175 | if (!isOpen) return null; 176 | 177 | const countModelsPerFamily = (models: string[]): { [key: string]: number } => { 178 | const counts: { [key: string]: number } = {}; 179 | for (const model of models) { 180 | const family = identifyModelFamily(model); 181 | if (family) { 182 | counts[family] = (counts[family] || 0) + 1; 183 | } 184 | } 185 | return counts; 186 | }; 187 | 188 | const modelCounts = countModelsPerFamily(availableModels); 189 | const sortedModelFamilies = Object.entries(modelFamilies) 190 | .sort(([a], [b]) => (modelCounts[b] || 0) - (modelCounts[a] || 0)); 191 | 192 | return ( 193 |
197 |
198 |
199 |

Select Model

200 | 203 |
204 |
205 | 210 | 215 |
216 | ( 219 | 231 | )} 232 | /> 233 |
234 |
235 | ); 236 | }; 237 | 238 | export default ModelSearch; -------------------------------------------------------------------------------- /src/components/ModelSearch/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Search } from 'lucide-react'; 3 | 4 | interface SearchInputProps { 5 | searchTerm: string; 6 | setSearchTerm: (term: string) => void; 7 | inputRef: React.RefObject; 8 | } 9 | 10 | const SearchInput: React.FC = ({ searchTerm, setSearchTerm, inputRef }) => { 11 | return ( 12 |
13 | setSearchTerm(e.target.value)} 19 | className="w-full p-2 pl-8 bg-[var(--bg-color)] border border-[var(--border-color)] rounded-md text-gray-200 focus:outline-none focus:ring-2 focus:ring-[var(--border-color)] transition-all" 20 | /> 21 | 22 |
23 | ); 24 | }; 25 | 26 | export default SearchInput; 27 | -------------------------------------------------------------------------------- /src/components/ModelSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { ChevronDown } from 'lucide-react'; 3 | import ModelSearch from './ModelSearch/ModelSearch'; 4 | import { modelDetailsList } from '../utils/llm'; 5 | 6 | interface ModelSelectorProps { 7 | selectedModel: string; 8 | setSelectedModel: (model: string) => void; 9 | onModelLoad: () => void; 10 | isModelLoaded: boolean; 11 | availableModels: string[]; 12 | loadProgress: string; 13 | } 14 | 15 | const ModelSelector: React.FC = ({ 16 | selectedModel, 17 | setSelectedModel, 18 | onModelLoad, 19 | isModelLoaded, 20 | availableModels, 21 | loadProgress 22 | }) => { 23 | const [isModalOpen, setIsModalOpen] = useState(false); 24 | 25 | const openModal = useCallback(() => setIsModalOpen(true), []); 26 | const closeModal = useCallback(() => setIsModalOpen(false), []); 27 | 28 | // Find the icon for the selected model 29 | const selectedModelDetails = modelDetailsList.find(model => selectedModel.toLowerCase().includes(model.name)); 30 | const SelectedModelIcon = selectedModelDetails ? selectedModelDetails.icon : null; 31 | 32 | // Handle keyboard shortcuts 33 | useEffect(() => { 34 | const handleKeyDown = (event: KeyboardEvent) => { 35 | if ((event.metaKey || event.ctrlKey) && event.key === 'k') { 36 | event.preventDefault(); 37 | openModal(); 38 | } else if (event.key === 'Escape' && isModalOpen) { 39 | closeModal(); 40 | } 41 | }; 42 | 43 | window.addEventListener('keydown', handleKeyDown); 44 | return () => window.removeEventListener('keydown', handleKeyDown); 45 | }, [openModal, closeModal, isModalOpen]); 46 | 47 | return ( 48 |
49 |

Models

50 |
51 | 64 | 71 |
72 | {!isModelLoaded && ( 73 |

74 | First download may take a little bit. Subsequent loads will read from cache. 75 |

76 | )} 77 | {loadProgress && ( 78 |
{loadProgress}
79 | )} 80 | 86 |
87 | ); 88 | }; 89 | 90 | export default ModelSelector; -------------------------------------------------------------------------------- /src/components/icons/GoogleIcon.tsx: -------------------------------------------------------------------------------- 1 | const GoogleIcon = ({ className = 'w-8 h-8' }) => { 2 | return ( 3 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | export default GoogleIcon; -------------------------------------------------------------------------------- /src/components/icons/MetaIcon.tsx: -------------------------------------------------------------------------------- 1 | const MetaLogo = ({ className = 'w-8 h-8' }) => { 2 | return ( 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default MetaLogo; -------------------------------------------------------------------------------- /src/components/icons/MicrosoftIcon.tsx: -------------------------------------------------------------------------------- 1 | const MicrosoftIcon = ({ className = 'w-8 h-8' }) => { 2 | return ( 3 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default MicrosoftIcon; 20 | -------------------------------------------------------------------------------- /src/components/icons/MistralIcon.tsx: -------------------------------------------------------------------------------- 1 | const MistralIcon = ({ className = 'w-8 h-8' }) => { 2 | return ( 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default MistralIcon; -------------------------------------------------------------------------------- /src/components/icons/QwenIcon.tsx: -------------------------------------------------------------------------------- 1 | const QwenIcon = ({ className = 'w-8 h-8' }) => { 2 | return ( 3 | Qwen Logo 10 | ); 11 | }; 12 | 13 | export default QwenIcon; 14 | -------------------------------------------------------------------------------- /src/components/icons/SmolLMIcon.tsx: -------------------------------------------------------------------------------- 1 | const SmolLMIcon = ({ className = 'w-8 h-8' }) => { 2 | return ( 3 | SmolLM Logo 10 | ); 11 | }; 12 | 13 | export default SmolLMIcon; 14 | -------------------------------------------------------------------------------- /src/components/icons/SnowflakeIcon.tsx: -------------------------------------------------------------------------------- 1 | const SnowflakeIcon = ({ className = 'w-8 h-8' }) => { 2 | return ( 3 | 10 | Snowflake Logo 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default SnowflakeIcon; -------------------------------------------------------------------------------- /src/components/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MetaIcon } from './MetaIcon'; 2 | export { default as MicrosoftIcon } from './MicrosoftIcon'; 3 | export { default as MistralIcon } from './MistralIcon'; 4 | export { default as GoogleIcon } from './GoogleIcon'; 5 | export { default as SnowflakeIcon } from './SnowflakeIcon'; 6 | export { default as QwenIcon } from './QwenIcon'; 7 | export { default as SmolLMIcon } from './SmolLMIcon'; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | :root { 7 | --border-color: #2F2E2E; 8 | --bg-color: #171717; 9 | } 10 | 11 | .no-scrollbar { 12 | scrollbar-width: none; 13 | } 14 | 15 | .custom-scrollbar::-webkit-scrollbar { 16 | width: 0px; 17 | } 18 | 19 | /* Webkit browsers (Chrome, Safari, newer versions of Edge) */ 20 | .custom-scrollbar::-webkit-scrollbar { 21 | width: 10px; 22 | } 23 | 24 | .custom-scrollbar::-webkit-scrollbar-track { 25 | background: var(--bg-color); 26 | } 27 | 28 | .custom-scrollbar::-webkit-scrollbar-thumb { 29 | background-color: #4a5568; 30 | border-radius: 6px; 31 | border: 3px solid var(--bg-color); 32 | } 33 | 34 | .custom-scrollbar::-webkit-scrollbar-thumb:hover { 35 | background-color: #718096; 36 | } 37 | 38 | /* Firefox */ 39 | .custom-scrollbar { 40 | scrollbar-width: thin; 41 | scrollbar-color: #4a5568 var(--bg-color); 42 | } 43 | 44 | /* For Internet Explorer and Edge (older versions) */ 45 | .custom-scrollbar { 46 | -ms-overflow-style: none; 47 | } -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /src/utils/llm.ts: -------------------------------------------------------------------------------- 1 | import * as webllm from "@mlc-ai/web-llm"; 2 | import { 3 | MetaIcon, 4 | MicrosoftIcon, 5 | MistralIcon, 6 | GoogleIcon, 7 | QwenIcon, 8 | SmolLMIcon, 9 | } from '../components/icons'; 10 | 11 | export interface ModelDetails { 12 | name: string; 13 | icon: React.ComponentType>; 14 | } 15 | 16 | export const modelDetailsList: ModelDetails[] = [ 17 | { name: 'llama', icon: MetaIcon }, 18 | { name: 'phi', icon: MicrosoftIcon }, 19 | { name: 'mistral', icon: MistralIcon }, 20 | { name: 'gemma', icon: GoogleIcon }, 21 | { name: 'qwen', icon: QwenIcon }, 22 | { name: 'smollm', icon: SmolLMIcon }, 23 | ]; 24 | 25 | let engine: webllm.MLCEngine | null = null; 26 | let progressCallback: ((progress: string) => void) | null = null; 27 | 28 | export async function initializeWebLLMEngine( 29 | selectedModel: string, 30 | temperature: number, 31 | topP: number, 32 | onCompletion: () => void 33 | ): Promise { 34 | try { 35 | engine = new webllm.MLCEngine(); 36 | engine.setInitProgressCallback(handleEngineInitProgress); 37 | 38 | const config = { temperature, top_p: topP }; 39 | await engine.reload(selectedModel, config); 40 | onCompletion(); 41 | } catch (error) { 42 | console.error("Error loading model:", error); 43 | throw error; 44 | } 45 | } 46 | 47 | export function setProgressCallback(callback: (progress: string) => void): void { 48 | progressCallback = callback; 49 | } 50 | 51 | function handleEngineInitProgress(report: { text: string }): void { 52 | if (progressCallback) { 53 | progressCallback(report.text); 54 | } 55 | } 56 | 57 | export async function streamingGenerating( 58 | messages: webllm.ChatCompletionMessageParam[], 59 | onUpdate: (currentMessage: string) => void, 60 | onFinish: (finalMessage: string, usage: webllm.CompletionUsage) => void, 61 | onError: (error: Error) => void 62 | ): Promise { 63 | if (!engine) { 64 | onError(new Error("Engine not initialized")); 65 | return; 66 | } 67 | 68 | try { 69 | let currentMessage = ""; 70 | let usage: webllm.CompletionUsage | undefined; 71 | 72 | const completion = await engine.chat.completions.create({ 73 | stream: true, 74 | messages, 75 | stream_options: { include_usage: true }, 76 | }); 77 | 78 | for await (const chunk of completion) { 79 | const delta = chunk.choices[0]?.delta.content; 80 | if (delta) currentMessage += delta; 81 | if (chunk.usage) { 82 | usage = chunk.usage; 83 | } 84 | onUpdate(currentMessage); 85 | } 86 | 87 | const finalMessage = await engine.getMessage(); 88 | if (usage) { 89 | onFinish(finalMessage, usage as webllm.CompletionUsage); 90 | } else { 91 | throw new Error("Usage data not available"); 92 | } 93 | } catch (error) { 94 | onError(error as Error); 95 | } 96 | } 97 | export const availableModels: string[] = webllm.prebuiltAppConfig.model_list 98 | .filter((model) => model.model_type !== 1 && model.model_type !== 2) // filter out embedding / vlms (https://github.com/mlc-ai/web-llm/blob/a24213cda0013e1772be7084bb8cbfdfec8af407/src/config.ts#L229-L233) 99 | .map((model) => model.model_id); 100 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./src/**/*.{js,jsx,ts,tsx}", 5 | ], 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | inter: ['Inter', 'sans-serif'], 10 | }, 11 | }, 12 | }, 13 | plugins: [require('@tailwindcss/typography')], 14 | } -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.app.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/app.tsx","./src/main.tsx","./src/components/chatbox.tsx","./src/components/chatheader.tsx","./src/components/chatstats.tsx","./src/components/header.tsx","./src/components/inputarea.tsx","./src/components/modelselector.tsx","./src/components/modelsearch/familyfilter.tsx","./src/components/modelsearch/modellist.tsx","./src/components/modelsearch/modelrow.tsx","./src/components/modelsearch/modelsearch.tsx","./src/components/modelsearch/searchinput.tsx","./src/components/icons/googleicon.tsx","./src/components/icons/metaicon.tsx","./src/components/icons/microsofticon.tsx","./src/components/icons/mistralicon.tsx","./src/components/icons/qwenicon.tsx","./src/components/icons/smollmicon.tsx","./src/components/icons/snowflakeicon.tsx","./src/components/icons/index.ts","./src/utils/llm.ts"],"version":"5.6.2"} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.node.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./vite.config.ts"],"version":"5.6.2"} -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /webllm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfahlgren1/webllm-playground/1a61c58f69413f84126b2443bba5fc3ef75c869b/webllm.gif --------------------------------------------------------------------------------