Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Integrate React+Vite web apps with bodhi-js-sdk for local LLM integration. Use when user asks to: "integrate bodhi", "add bodhi sdk", "connect to bodhi", "setup bodhi provider", "bodhi react integration", "deploy bodhi to github pages", or troubleshoot bodhi-js-sdk connection/auth issues.
.claude/skills/aiskillstore-bodhi-sdk-react-integration/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 50% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 71% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 90% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 126% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 131% | 0% |
Guide for integrating React+Vite applications with bodhi-js-sdk to enable local LLM chat capabilities through the Bodhi Browser ecosystem.
npm install @bodhiapp/bodhi-js-react<BodhiProvider authClientId={...}> around your appuseBodhi() for client, auth state, and actions@bodhiapp/bodhi-js-react - Preset package for web apps (auto-creates WebUIClient)@bodhiapp/bodhi-js-react-ext - Preset package for Chrome extensions (auto-creates ExtUIClient)https://main-id.getbodhi.app/realms/bodhi (allows localhost)https://id.getbodhi.app/realms/bodhi (requires real domain)bashnpm install @bodhiapp/bodhi-js-react
tsx// App.tsx import { BodhiProvider } from '@bodhiapp/bodhi-js-react'; import Chat from './Chat'; const CLIENT_ID = 'your-client-id-from-developer.getbodhi.app'; function App() { return ( <BodhiProvider authClientId={CLIENT_ID}> <div className="app"> <h1>My Bodhi Chat App</h1> <Chat /> </div> </BodhiProvider> ); } export default App;
tsx// Chat.tsx import { useState, useEffect } from 'react'; import { useBodhi } from '@bodhiapp/bodhi-js-react'; function Chat() { const { client, isOverallReady, isAuthenticated, login, showSetup } = useBodhi(); const [prompt, setPrompt] = useState(''); const [response, setResponse] = useState(''); const [loading, setLoading] = useState(false); const [models, setModels] = useState<string[]>([]); const [selectedModel, setSelectedModel] = useState(''); // Load models on mount useEffect(() => { if (isOverallReady && isAuthenticated) { loadModels(); } }, [isOverallReady, isAuthenticated]); const loadModels = async () => { const modelList: string[] = []; for await (const model of client.models.list()) { modelList.push(model.id); } setModels(modelList); if (modelList.length > 0) setSelectedModel(modelList[0]); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!prompt.trim() || !selectedModel) return; setLoading(true); setResponse(''); try { const stream = client.chat.completions.create({ model: selectedModel, messages: [{ role: 'user', content: prompt }], stream: true, }); for await (const chunk of stream) { const content = chunk.choices?.[0]?.delta?.content || ''; setResponse(prev => prev + content); } } catch (err) { setResponse(`Error: ${err instanceof Error ? err.message : String(err)}`); } finally { setLoading(false); } }; if (!isOverallReady) { return <button onClick={showSetup}>Open Setup</button>; } if (!isAuthenticated) { return <button onClick={login}>Login</button>; } return ( <div> <select value={selectedModel} onChange={e => setSelectedModel(e.target.value)}> {models.map(model => ( <option key={model} value={model}> {model} </option> ))} </select> <form onSubmit={handleSubmit}> <input value={prompt} onChange={e => setPrompt(e.target.value)} /> <button type="submit" disabled={loading}> {loading ? 'Generating...' : 'Send'} </button> </form> {response && <div>{response}</div>} </div> ); } export default Chat;
tsxconst { client, // SDK client instance (OpenAI-compatible API) isOverallReady, // Both client AND server ready (most common check) isAuthenticated, // User has valid OAuth token login, // Initiate OAuth login flow logout, // Logout and clear tokens showSetup, // Open setup wizard modal // Additional properties isReady, // Client initialized (extension or direct URL) isServerReady, // Server status is 'ready' isInitializing, // client.init() in progress isExtension, // Using extension mode isDirect, // Using direct HTTP mode canLogin, // isReady && !isAuthLoading isAuthLoading, // Auth operation in progress } = useBodhi();
tsx// List models (AsyncGenerator) for await (const model of client.models.list()) { console.log(model.id); } // Streaming chat const stream = client.chat.completions.create({ model: 'gemma-3n-e4b-it', messages: [{ role: 'user', content: 'Hello!' }], stream: true, }); for await (const chunk of stream) { const content = chunk.choices?.[0]?.delta?.content || ''; // Append to response } // Non-streaming chat const response = await client.chat.completions.create({ model: 'gemma-3n-e4b-it', messages: [{ role: 'user', content: 'Hello!' }], stream: false, });
tsx<BodhiProvider authClientId={CLIENT_ID} clientConfig={{ redirectUri: 'https://myapp.com/callback', basePath: '/app', logLevel: 'debug', }} > <App /> </BodhiProvider>
When your app runs on a sub-path (e.g., GitHub Pages at /repo-name/):
tsx// Vite config export default defineConfig({ base: '/repo-name/', }); // BodhiProvider <BodhiProvider authClientId={CLIENT_ID} basePath="/repo-name" callbackPath="/repo-name/callback"> <App /> </BodhiProvider>;
tsxfunction App() { const { isOverallReady, isAuthenticated, showSetup, login } = useBodhi(); if (!isOverallReady) { return <button onClick={showSetup}>Setup Required</button>; } if (!isAuthenticated) { return <button onClick={login}>Login Required</button>; } return <ChatInterface />; }
tsxconst loadModels = async () => { const cached = localStorage.getItem('bodhi_models'); if (cached) { const { models: cachedModels, expiry } = JSON.parse(cached); if (Date.now() < expiry) { setModels(cachedModels); return; } } const modelList: string[] = []; for await (const model of client.models.list()) { modelList.push(model.id); } setModels(modelList); localStorage.setItem( 'bodhi_models', JSON.stringify({ models: modelList, expiry: Date.now() + 3600000, // 1 hour }) ); };
tsxtry { const stream = client.chat.completions.create({ ... }); for await (const chunk of stream) { // Process chunk } } catch (err) { if (err instanceof Error) { console.error('Chat error:', err.message); setError(err.message); } }
For comprehensive information on specific topics, see the supporting documentation:
The bodhi-js-sdk repository contains comprehensive documentation:
bodhi-js-sdk/docs/quick-start.md - Official quick startbodhi-js-sdk/docs/react-integration.md - Deep dive into React integrationbodhi-js-sdk/docs/authentication.md - OAuth flow detailsbodhi-js-sdk/docs/streaming.md - Streaming patternsbodhi-js-sdk/docs/api-reference.md - Complete API documentationWhen user asks to integrate bodhi-js-sdk:
npm install @bodhiapp/bodhi-js-reactWhen troubleshooting:
isOverallReady, isReady, isServerReadyisAuthenticated, auth object[Bodhi/Web] prefixed logs in consoleAfter integration, verify:
[Bodhi/Web] Extension detected[Bodhi/Web] Server readyWhen user requests:
When implementing integration:
bodhi-js-sdk/docs/quick-start.md - Primary integration guidebodhi-js-sdk/docs/react-integration.md - React-specific patternsbodhi-js-sdk/docs/ - Comprehensive documentation and examples@bodhiapp/bodhi-js-react preset package (simplest)handleCallback={true})| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 20,966 | 21,806 | +4% | 1 | 1 | 0% | 4,569 | 6,860 | +50% | 0 | 0 | — |
case-02 | fail→fail | 16,721 | 10,622 | -36% | 1 | 1 | 0% | 3,083 | 5,243 | +70% | 0 | 0 | — |
case-03 | fail→pass | 21,438 | 16,470 | -23% | 1 | 1 | 0% | 3,157 | 5,405 | +71% | 0 | 0 | — |
case-04 | fail→pass | 12,976 | 3,836 | -70% | 1 | 1 | 0% | 2,001 | 3,793 | +90% | 0 | 0 | — |
case-05 | fail→pass | 11,599 | 10,007 | -14% | 1 | 1 | 0% | 1,851 | 4,177 | +126% | 0 | 0 | — |
case-06 | fail→pass | 10,040 | 9,100 | -9% | 1 | 1 | 0% | 1,613 | 3,729 | +131% | 0 | 0 | — |
case-07 | fail→pass | 15,404 | 3,096 | -80% | 1 | 1 | 0% | 1,823 | 3,629 | +99% | 0 | 0 | — |
case-08 | pass→pass | 27,914 | 2,248 | -92% | 1 | 1 | 0% | 4,230 | 3,432 | -19% | 0 | 0 | — |
case-09 | fail→pass | 7,191 | 7,825 | +9% | 1 | 1 | 0% | 1,197 | 3,584 | +199% | 0 | 0 | — |
case-10 | pass→pass | 22,702 | 8,652 | -62% | 1 | 1 | 0% | 2,127 | 4,948 | +133% | 0 | 0 | — |
case-11 | fail→pass | 14,985 | 13,585 | -9% | 1 | 1 | 0% | 1,741 | 4,862 | +179% | 0 | 0 | — |
case-12 | fail→pass | 12,661 | 2,489 | -80% | 1 | 1 | 0% | 1,171 | 3,533 | +202% | 0 | 0 | — |
case-13 | fail→pass | 15,007 | 2,718 | -82% | 1 | 1 | 0% | 1,853 | 3,561 | +92% | 0 | 0 | — |
case-14 | pass→pass | 11,374 | 7,602 | -33% | 1 | 1 | 0% | 1,857 | 3,529 | +90% | 0 | 0 | — |
case-15 | fail→pass | 16,499 | 2,752 | -83% | 1 | 1 | 0% | 1,653 | 3,512 | +112% | 0 | 0 | — |
case-16 | fail→pass | 17,312 | 12,977 | -25% | 1 | 1 | 0% | 2,353 | 5,648 | +140% | 0 | 0 | — |
case-17 | fail→pass | 11,467 | 7,275 | -37% | 1 | 1 | 0% | 1,146 | 4,764 | +316% | 0 | 0 | — |
case-18 | fail→pass | 19,632 | 7,770 | -60% | 1 | 1 | 0% | 2,396 | 3,609 | +51% | 0 | 0 | — |
case-19 | fail→pass | 17,297 | 7,898 | -54% | 1 | 1 | 0% | 2,013 | 3,678 | +83% | 0 | 0 | — |
case-20 | pass→fail | 15,758 | 11,342 | -28% | 1 | 1 | 0% | 2,065 | 5,319 | +158% | 0 | 0 | — |
case-21 | pass→pass | 4,915 | 6,039 | +23% | 1 | 1 | 0% | 922 | 4,218 | +357% | 0 | 0 | — |
case-22 | pass→fail | 17,797 | 15,789 | -11% | 1 | 1 | 0% | 2,380 | 5,267 | +121% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +59 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.