Add Chatwoot support widget and AI API setup documentation
- Insert Chatwoot chat widget script at bottom of index.html body (connects to chat.hoaledgeriq.com with token 1QMW1fycL5xHvd6XMfg4Dbb4) - Add AI_SETUP.md documenting how to upgrade the benefit calculator from client-side math to live AI API calls (Claude or OpenAI), including endpoint code, app.js changes, prompt tuning, cost estimates, and rate limiting guidance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
219
AI_SETUP.md
Normal file
219
AI_SETUP.md
Normal file
@@ -0,0 +1,219 @@
|
||||
# HOA LedgerIQ — AI API Configuration Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The **Benefit Calculator widget** currently runs entirely client-side using conservative
|
||||
fixed-rate math. This guide explains how to upgrade it to call a real AI API
|
||||
(Claude or OpenAI) so the recommendation text is generated dynamically by a language model.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Browser (app.js)
|
||||
└─► POST /api/calculate (server.js)
|
||||
└─► Anthropic / OpenAI API
|
||||
└─► Returns AI-generated investment recommendation text
|
||||
└─► JSON response back to browser
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Add your API key to `.env`
|
||||
|
||||
Open `.env` in the project root and add one of the following:
|
||||
|
||||
```env
|
||||
# For Claude (Anthropic) — recommended
|
||||
ANTHROPIC_API_KEY=sk-ant-...
|
||||
|
||||
# OR for OpenAI
|
||||
OPENAI_API_KEY=sk-...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Install the SDK
|
||||
|
||||
```bash
|
||||
# Claude (Anthropic)
|
||||
npm install @anthropic-ai/sdk
|
||||
|
||||
# OR OpenAI
|
||||
npm install openai
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Add the `/api/calculate` endpoint to `server.js`
|
||||
|
||||
Add this block after the existing `/api/health` route:
|
||||
|
||||
### Using Claude (Anthropic)
|
||||
|
||||
```js
|
||||
const Anthropic = require('@anthropic-ai/sdk');
|
||||
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
|
||||
|
||||
app.post('/api/calculate', async (req, res) => {
|
||||
const { homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025 } = req.body ?? {};
|
||||
|
||||
if (!homesites || !annualIncome) {
|
||||
return res.status(400).json({ error: 'homesites and annualIncome are required.' });
|
||||
}
|
||||
|
||||
const fmt = n => '$' + Math.round(n).toLocaleString();
|
||||
const freqLabel = { monthly: 'monthly', quarterly: 'quarterly', annually: 'annual' }[paymentFreq] || 'monthly';
|
||||
const typeLabel = { sfh: 'single-family home', townhomes: 'townhome', condos: 'condo', mixed: 'mixed-use' }[propertyType] || '';
|
||||
|
||||
const prompt = `You are a conservative HOA financial advisor. Given the following community data, provide a brief (3-4 sentence) plain-English investment income recommendation. Use only conservative, realistic estimates. Do not speculate beyond what the data supports.
|
||||
|
||||
Community: ${homesites}-unit ${typeLabel} association
|
||||
Annual dues income: ${fmt(annualIncome)} (collected ${freqLabel})
|
||||
Reserve fund balance: ${fmt(reserveFunds || 0)}
|
||||
Interest income earned in 2025: ${fmt(interest2025 || 0)}
|
||||
|
||||
Provide a recommendation focused on:
|
||||
1. How much of the reserve funds could conservatively be invested and in what vehicle (e.g. CD ladder, money market, T-bills)
|
||||
2. How much operating cash could earn interest between collection and expense periods
|
||||
3. A realistic estimated annual interest income potential
|
||||
4. A single sentence comparing that to their 2025 actual if provided
|
||||
|
||||
Keep the tone professional and factual. No bullet points — flowing paragraph only.`;
|
||||
|
||||
try {
|
||||
const message = await anthropic.messages.create({
|
||||
model: 'claude-opus-4-6',
|
||||
max_tokens: 300,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
||||
const text = message.content[0]?.text ?? '';
|
||||
res.json({ recommendation: text });
|
||||
} catch (err) {
|
||||
console.error('AI API error:', err.message);
|
||||
res.status(502).json({ error: 'AI service unavailable. Showing estimated result.' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Using OpenAI (GPT-4o)
|
||||
|
||||
```js
|
||||
const OpenAI = require('openai');
|
||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
|
||||
app.post('/api/calculate', async (req, res) => {
|
||||
const { homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025 } = req.body ?? {};
|
||||
|
||||
// ... (same prompt construction as above) ...
|
||||
|
||||
try {
|
||||
const completion = await openai.chat.completions.create({
|
||||
model: 'gpt-4o',
|
||||
max_tokens: 300,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
||||
const text = completion.choices[0]?.message?.content ?? '';
|
||||
res.json({ recommendation: text });
|
||||
} catch (err) {
|
||||
console.error('AI API error:', err.message);
|
||||
res.status(502).json({ error: 'AI service unavailable.' });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4 — Update `app.js` to call the API endpoint
|
||||
|
||||
In the `initCalculator` function, replace this line in the submitBtn handler:
|
||||
|
||||
```js
|
||||
document.getElementById('calcAiText').textContent = ai; // current: client-side text
|
||||
```
|
||||
|
||||
With this:
|
||||
|
||||
```js
|
||||
// Call the AI endpoint; fall back to client-side text if unavailable
|
||||
try {
|
||||
const aiRes = await fetch('/api/calculate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ homesites, propertyType, annualIncome, paymentFreq, reserveFunds, interest2025 }),
|
||||
});
|
||||
if (aiRes.ok) {
|
||||
const { recommendation } = await aiRes.json();
|
||||
document.getElementById('calcAiText').textContent = recommendation;
|
||||
} else {
|
||||
document.getElementById('calcAiText').textContent = ai; // fallback
|
||||
}
|
||||
} catch (_) {
|
||||
document.getElementById('calcAiText').textContent = ai; // fallback
|
||||
}
|
||||
```
|
||||
|
||||
> Note: The `submitBtn` handler must be declared `async` for the `await` above to work:
|
||||
> ```js
|
||||
> submitBtn?.addEventListener('click', async () => { ... });
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — Restart the server
|
||||
|
||||
```bash
|
||||
sudo systemctl restart hoaledgeriqweb
|
||||
|
||||
# Verify the endpoint is live
|
||||
curl -X POST http://localhost:3000/api/calculate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"homesites":150,"propertyType":"sfh","annualIncome":300000,"paymentFreq":"monthly","reserveFunds":500000,"interest2025":4200}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prompt Tuning Tips
|
||||
|
||||
The prompt in Step 3 is the core of the AI's behavior. You can adjust it to:
|
||||
|
||||
| Goal | Change |
|
||||
|---|---|
|
||||
| More optimistic estimates | Change "conservative" to "moderate" in the prompt |
|
||||
| Shorter output | Reduce `max_tokens` to `150` |
|
||||
| Include specific investment products | Add "mention specific products like Vanguard Federal Money Market or 6-month T-bills" |
|
||||
| Add a disclaimer | Append "End with one sentence reminding them this is not financial advice." |
|
||||
|
||||
---
|
||||
|
||||
## Cost Estimate
|
||||
|
||||
| Model | Approx. cost per calculator use |
|
||||
|---|---|
|
||||
| Claude Opus 4.6 | ~$0.002 |
|
||||
| Claude Sonnet 4.6 | ~$0.0004 |
|
||||
| GPT-4o | ~$0.002 |
|
||||
| GPT-4o-mini | ~$0.00005 |
|
||||
|
||||
For a landing page with low traffic, even Claude Opus is negligible cost. For scale,
|
||||
`claude-sonnet-4-6` is the best balance of quality and price.
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- **Never expose your API key in `app.js` or any client-side code.** All AI calls must go through `server.js`.
|
||||
- Rate-limit the `/api/calculate` endpoint to prevent abuse (e.g. with `express-rate-limit`):
|
||||
|
||||
```bash
|
||||
npm install express-rate-limit
|
||||
```
|
||||
|
||||
```js
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const calcLimiter = rateLimit({ windowMs: 60 * 1000, max: 10 }); // 10 req/min per IP
|
||||
app.use('/api/calculate', calcLimiter);
|
||||
```
|
||||
Reference in New Issue
Block a user