Skip to content

Commit c7faf85

Browse files
Implement Tool 1 backend, frontend wiring, and full README
- Worker: POST /generate proxy to Anthropic /v1/messages with claude-sonnet-4-6, CORS-locked to the Pages origin, ANTHROPIC_API_KEY read from a Wrangler secret. Handles 4xx/5xx with friendly messages - wrangler.toml: minimal config + inline deploy/secret instructions - prompts/meeting-brief.md: canonical 5-section brief prompt template - app.js: form submit -> build prompt -> POST to Worker -> render Markdown (inline parser, no deps); loading + error states; detects unconfigured WORKER_URL and shows a maintenance message - index.html: hide output card by default, drop the hardcoded sample (real Claude output replaces it once the Worker is wired) - style.css: small loading + error state styling - README: architecture diagram, fork-and-deploy walkthrough, privacy note, roadmap WORKER_URL in app.js is still a placeholder; the Worker has to be deployed and the URL pasted in before the live site can generate. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e6df754 commit c7faf85

7 files changed

Lines changed: 503 additions & 55 deletions

File tree

README.md

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,109 @@
11
# EA Toolkit
22

3-
A small set of AI tools for executive assistants — static frontend, Cloudflare Workers proxy, no build step.
3+
Small AI tools for executive assistants. Static frontend, Cloudflare Workers proxy, no build step.
44

5-
**Status:** mid-rebuild. The Vite + React v1 lives on at [`archive/v1-react`](https://github.com/lisaesterhuizen0-wq/ea-toolkit/tree/archive/v1-react). The current `main` is being rebuilt as a static toolkit. Tool 1 (Meeting Brief Generator) is the first to ship.
5+
**Live:** https://lisaesterhuizen0-wq.github.io/ea-toolkit/
66

7-
Full README — architecture, fork-and-deploy, live URL — arrives once Tool 1 is wired end-to-end.
7+
## Status
8+
9+
| # | Tool | Status |
10+
|---|------|--------|
11+
| 01 | Meeting Brief Generator | **Live** |
12+
| 02 | EOD Summary Generator | Coming soon |
13+
| 03 | Email Triage Helper | Coming soon |
14+
| 04 | Calendar Conflict Negotiator | Coming soon |
15+
16+
## Why
17+
18+
Most executive-assistant prep work is the same shape every time: take some context, distil signal, hand the exec something useful in 10 minutes. The four tools here each take a flavour of that work and give it a tight UI plus a well-tuned prompt.
19+
20+
## What's in this repo
21+
22+
```
23+
index.html # 4-tab toolkit page; only Tool 1 active
24+
style.css # design system (cream paper / jade / jasper)
25+
app.js # vanilla JS — submit, fetch, Markdown render
26+
prompts/
27+
├── meeting-brief.md # Tool 1's prompt template (canonical reference)
28+
└── legacy/ # 5 prompts from the v1 React build, kept as a library
29+
worker/
30+
├── wrangler.toml # Cloudflare Worker config
31+
└── src/index.js # the proxy — POST /generate → Anthropic API
32+
```
33+
34+
The previous Vite + React build is preserved on the [`archive/v1-react`](https://github.com/lisaesterhuizen0-wq/ea-toolkit/tree/archive/v1-react) branch.
35+
36+
## How it works
37+
38+
```
39+
[ Browser ] [ Cloudflare Worker ] [ Anthropic API ]
40+
GitHub Pages ea-toolkit-proxy api.anthropic.com
41+
│ │ │
42+
│ POST /generate { prompt } │ │
43+
│ ───────────────────────────▶ │ │
44+
│ │ POST /v1/messages │
45+
│ │ + ANTHROPIC_API_KEY (secret) │
46+
│ │ ───────────────────────────────▶ │
47+
│ │ │
48+
│ │ ◀───────────── { text, ... } │
49+
│ ◀───────── { text } │ │
50+
│ │ │
51+
▼ ▼ ▼
52+
Renders Markdown CORS-locked to Pages origin Sees the prompt
53+
inline (no deps) Holds the API key Doesn't see the user
54+
```
55+
56+
The Worker is the only place the Anthropic API key lives. It is set as a Wrangler secret (encrypted, not visible after upload), CORS-locked to a single origin, and stateless — no database, no logs of user input.
57+
58+
## Privacy
59+
60+
Inputs you paste are sent once to Anthropic to generate the brief, then dropped. The Worker keeps no logs of prompt content. The frontend stores nothing in `localStorage` or `sessionStorage`.
61+
62+
## Run your own copy
63+
64+
You'll need: a GitHub account, a Cloudflare account (free tier is fine), an Anthropic API key, and Node.js for `wrangler`.
65+
66+
**1. Fork and enable Pages**
67+
68+
Fork this repo, then in your fork: **Settings → Pages → Source: Deploy from a branch → `main` / `/`**. Your fork will serve at `https://<your-username>.github.io/ea-toolkit/`.
69+
70+
**2. Deploy the Worker**
71+
72+
```bash
73+
cd worker
74+
npm install -g wrangler # one-time
75+
wrangler login # opens a browser
76+
wrangler deploy # prints your Worker URL
77+
wrangler secret put ANTHROPIC_API_KEY # paste your key when prompted
78+
```
79+
80+
**3. Lock the Worker to your origin**
81+
82+
In `worker/src/index.js`, change `ALLOWED_ORIGIN` from `https://lisaesterhuizen0-wq.github.io` to your own Pages origin (e.g. `https://<your-username>.github.io`), then `wrangler deploy` again.
83+
84+
**4. Wire the frontend to your Worker**
85+
86+
In `app.js`, replace the placeholder `WORKER_URL` with the URL Wrangler printed (e.g. `https://ea-toolkit-proxy.<your-handle>.workers.dev`). Commit and push — GitHub Pages will rebuild in ~30 seconds.
87+
88+
**5. Get an Anthropic API key**
89+
90+
Sign up at [console.anthropic.com](https://console.anthropic.com). Add a small amount of credit to enable API calls. Brief generation costs roughly $0.01–0.03 per call on Sonnet 4.6 — well under a cent of cost for a typical brief.
91+
92+
## Stack
93+
94+
- HTML, CSS, vanilla JavaScript — no React, no Vite, no build step
95+
- Cloudflare Workers for the API proxy (free tier handles ~100k requests/day)
96+
- GitHub Pages for hosting (free)
97+
- Anthropic Claude Sonnet 4.6 via `/v1/messages`
98+
99+
## Roadmap
100+
101+
Tools 2–4 reuse the same design system, Worker, and deploy pipeline. Each adds one more tab to the front page.
102+
103+
- **EOD Summary Generator** — paste a day of emails / Slack / calendar; get a tight end-of-day note for the exec
104+
- **Email Triage Helper** — classify and draft replies for an inbox dump
105+
- **Calendar Conflict Negotiator** — generate a tactful reschedule message given an attendee list and a constraint
106+
107+
## Built by
108+
109+
[Lisa Myburgh](https://github.com/lisaesterhuizen0-wq) — AI Operations specialist. Part of a personal portfolio aimed at international remote AI Operations roles.

app.js

Lines changed: 217 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,226 @@
1-
// EA Toolkit — vanilla JS controller.
2-
// Step 2 (current): form-submit no-op for design review.
3-
// Step 4 (next session): replace handler with fetch-to-Worker + Markdown render.
1+
// EA Toolkit — Tool 1 (Meeting Brief Generator) frontend controller.
2+
//
3+
// Flow: form submit → build prompt → POST to Cloudflare Worker → render Markdown.
4+
// The Worker holds the Anthropic API key as a secret; the browser never sees it.
45

56
(function () {
67
'use strict';
78

9+
// ---- Configuration ---------------------------------------------------------
10+
// Paste the Cloudflare Worker URL here after running `wrangler deploy`.
11+
// Example: "https://ea-toolkit-proxy.lisa-myburgh.workers.dev"
12+
const WORKER_URL = "https://WORKER-URL-NOT-YET-CONFIGURED.workers.dev";
13+
14+
const PROMPT_TEMPLATE = `You are a senior executive assistant preparing a pre-meeting brief for an executive who has 10 minutes to skim it before a call.
15+
16+
CALENDAR / MEETING CONTEXT:
17+
{{meeting_context}}
18+
19+
PERSON THEY'RE MEETING:
20+
{{person_profile}}
21+
22+
Generate a brief with exactly these five sections, in this order, using the section headings shown.
23+
24+
## Who they are
25+
2–3 sentences distilled from the person's profile. Surface specific signal — current role, length in field, prior moves, public stances. Avoid generic descriptors.
26+
27+
## Meeting context
28+
2–4 sentences framing what's actually being discussed and what's likely beneath the surface. Separate the stated agenda from the implicit one. Be honest about ambiguity if the inputs don't reveal motive.
29+
30+
## Suggested talking points
31+
4–6 bullets. Each should be specific to this meeting and this person — not generic meeting advice. Lead with what your exec should establish, ask about, or be ready for.
32+
33+
## Questions to ask
34+
4–6 sharp, specific questions. Avoid questions answerable from the inputs already provided. Avoid yes/no questions. Each should open useful information.
35+
36+
## Red flags & sensitivities
37+
Anything to avoid or be careful around, drawn specifically from this profile or context (e.g. public stances they've taken, sensitive past projects, unusual signals in the invite). If the inputs reveal nothing concerning, write "None identified from the inputs provided." — do not invent.
38+
39+
Be concise. Use Markdown — \`## Heading\` for the five section titles, \`- bullet\` for lists. Do not include preamble before the first heading or summary after the last section. Do not wrap your output in code fences.`;
40+
41+
// ---- DOM refs --------------------------------------------------------------
842
const form = document.getElementById('brief-form');
9-
if (!form) return;
43+
const meetingInput = document.getElementById('meeting_context');
44+
const personInput = document.getElementById('person_profile');
45+
const submitBtn = document.getElementById('generate-btn');
46+
const outputSection = document.getElementById('output-section');
47+
const outputContent = document.getElementById('brief-content');
48+
const outputHeader = outputSection?.querySelector('.output-header');
49+
50+
if (!form || !meetingInput || !personInput || !submitBtn || !outputSection || !outputContent) {
51+
return;
52+
}
53+
54+
// ---- Helpers ---------------------------------------------------------------
55+
function escapeHtml(s) {
56+
return s
57+
.replace(/&/g, '&amp;')
58+
.replace(/</g, '&lt;')
59+
.replace(/>/g, '&gt;')
60+
.replace(/"/g, '&quot;')
61+
.replace(/'/g, '&#39;');
62+
}
63+
64+
function renderInline(text) {
65+
let s = escapeHtml(text);
66+
s = s.replace(/\*\*([^\n*][^\n]*?)\*\*/g, '<strong>$1</strong>');
67+
s = s.replace(/(^|[^\*])\*([^\n*][^\n]*?)\*(?!\*)/g, '$1<em>$2</em>');
68+
return s;
69+
}
70+
71+
function renderMarkdown(md) {
72+
const lines = md.replace(/\r\n/g, '\n').split('\n');
73+
const out = [];
74+
let listOpen = false;
75+
let paraBuf = [];
1076

11-
form.addEventListener('submit', function (e) {
77+
function flushPara() {
78+
if (paraBuf.length) {
79+
out.push(`<p>${renderInline(paraBuf.join(' '))}</p>`);
80+
paraBuf = [];
81+
}
82+
}
83+
function closeList() {
84+
if (listOpen) {
85+
out.push('</ul>');
86+
listOpen = false;
87+
}
88+
}
89+
90+
for (const raw of lines) {
91+
const line = raw.trimEnd();
92+
if (!line.trim()) {
93+
flushPara();
94+
closeList();
95+
continue;
96+
}
97+
const h2 = line.match(/^##\s+(.*)$/);
98+
const h3 = line.match(/^###\s+(.*)$/);
99+
const bullet = line.match(/^\s*[-]\s+(.+)$/);
100+
if (h2) {
101+
flushPara();
102+
closeList();
103+
out.push(`<h2>${renderInline(h2[1])}</h2>`);
104+
} else if (h3) {
105+
flushPara();
106+
closeList();
107+
out.push(`<h3>${renderInline(h3[1])}</h3>`);
108+
} else if (bullet) {
109+
flushPara();
110+
if (!listOpen) {
111+
out.push('<ul>');
112+
listOpen = true;
113+
}
114+
out.push(`<li>${renderInline(bullet[1])}</li>`);
115+
} else {
116+
closeList();
117+
paraBuf.push(line.trim());
118+
}
119+
}
120+
flushPara();
121+
closeList();
122+
return out.join('\n');
123+
}
124+
125+
function setLoading(isLoading) {
126+
submitBtn.classList.toggle('loading', isLoading);
127+
submitBtn.disabled = isLoading;
128+
submitBtn.querySelector('.btn-label').textContent = isLoading ? 'Generating' : 'Generate brief';
129+
meetingInput.disabled = isLoading;
130+
personInput.disabled = isLoading;
131+
}
132+
133+
function showOutput({ kind, html, headerEyebrow, headerMeta }) {
134+
if (outputHeader) {
135+
const eyebrow = outputHeader.querySelector('.eyebrow');
136+
const meta = outputHeader.querySelector('.output-meta');
137+
if (eyebrow && headerEyebrow) {
138+
const dot = '<span class="dot"></span>';
139+
eyebrow.innerHTML = `${dot}${escapeHtml(headerEyebrow)}`;
140+
}
141+
if (meta) meta.textContent = headerMeta || '';
142+
}
143+
outputContent.className = `brief brief--${kind}`;
144+
outputContent.innerHTML = html;
145+
outputSection.hidden = false;
146+
outputSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
147+
}
148+
149+
function showError(message) {
150+
showOutput({
151+
kind: 'error',
152+
html: `<p class="error-text">${escapeHtml(message)}</p>`,
153+
headerEyebrow: 'Error',
154+
headerMeta: 'Something went wrong generating the brief.',
155+
});
156+
}
157+
158+
function workerNotConfigured() {
159+
return WORKER_URL.includes('WORKER-URL-NOT-YET-CONFIGURED');
160+
}
161+
162+
// ---- Submit ----------------------------------------------------------------
163+
form.addEventListener('submit', async function (e) {
12164
e.preventDefault();
13-
// Wiring to the Cloudflare Worker lands in step 4.
165+
if (submitBtn.disabled) return;
166+
167+
const meeting = meetingInput.value.trim();
168+
const person = personInput.value.trim();
169+
if (!meeting || !person) {
170+
showError('Please fill in both fields before generating.');
171+
return;
172+
}
173+
174+
if (workerNotConfigured()) {
175+
showError(
176+
'The Cloudflare Worker URL has not been configured yet. ' +
177+
'This site is still being set up — check back shortly, or fork the repo and follow the README to deploy your own.'
178+
);
179+
return;
180+
}
181+
182+
const prompt = PROMPT_TEMPLATE
183+
.replace('{{meeting_context}}', meeting)
184+
.replace('{{person_profile}}', person);
185+
186+
setLoading(true);
187+
showOutput({
188+
kind: 'loading',
189+
html: '<p class="loading-text">Generating brief…</p>',
190+
headerEyebrow: 'Brief',
191+
headerMeta: 'Sending to Claude — usually takes 5–15 seconds.',
192+
});
193+
194+
try {
195+
const res = await fetch(WORKER_URL + '/generate', {
196+
method: 'POST',
197+
headers: { 'Content-Type': 'application/json' },
198+
body: JSON.stringify({ prompt }),
199+
});
200+
201+
const data = await res.json().catch(() => ({}));
202+
203+
if (!res.ok) {
204+
showError(data?.error || `Request failed (${res.status}). Please try again.`);
205+
return;
206+
}
207+
208+
const text = data?.text;
209+
if (typeof text !== 'string' || !text.trim()) {
210+
showError('The model returned an empty response. Please try again.');
211+
return;
212+
}
213+
214+
showOutput({
215+
kind: 'success',
216+
html: renderMarkdown(text),
217+
headerEyebrow: 'Brief',
218+
headerMeta: 'Generated by Claude. Review before sending to your exec.',
219+
});
220+
} catch (err) {
221+
showError("Couldn't reach the Worker. Check your connection and try again.");
222+
} finally {
223+
setLoading(false);
224+
}
14225
});
15226
})();

index.html

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -54,39 +54,12 @@ <h1>Pre-meeting <em>brief</em>, in seconds.</h1>
5454
</div>
5555
</form>
5656

57-
<section class="card output" id="output-section" aria-live="polite">
57+
<section class="card output" id="output-section" aria-live="polite" hidden>
5858
<header class="output-header">
59-
<p class="eyebrow"><span class="dot"></span>Sample output</p>
60-
<p class="output-meta">A real brief lands here once you hit Generate.</p>
59+
<p class="eyebrow"><span class="dot"></span>Brief</p>
60+
<p class="output-meta"></p>
6161
</header>
62-
<article class="brief" id="brief-content">
63-
<h2>Who they are</h2>
64-
<p>Marcus Lindqvist is Director of Sustainability at Cairnstone Carbon Group, a mid-sized climate consultancy. Twelve years in the field with prior stints at two policy think tanks. He's vocal on LinkedIn about quality concerns in the voluntary carbon market — particularly around project additionality.</p>
65-
66-
<h2>Meeting context</h2>
67-
<p>A 30-minute Tuesday Zoom positioned as exploratory. Marcus has flagged interest in "potential collaboration on Q3 carbon-procurement strategy", introduced via a sustainability-summit contact last month. Read: a soft-sales conversation framed as a brain-pick. Your exec wants to gauge whether there's substance behind the vague pitch before committing follow-up time.</p>
68-
69-
<h2>Suggested talking points</h2>
70-
<ul>
71-
<li>Frame your exec's current carbon-procurement posture before Marcus does — he'll otherwise lead.</li>
72-
<li>Reference the summit connection by name to anchor the relationship and set the right tone.</li>
73-
<li>Ask about Cairnstone's recent project portfolio — specific projects, not categories.</li>
74-
<li>Probe what "collaboration" actually means: consulting, co-investment, advisory, or something else.</li>
75-
<li>Be ready to deflect on Q3 timing if it isn't a real procurement window for your side.</li>
76-
</ul>
77-
78-
<h2>Questions to ask</h2>
79-
<ul>
80-
<li>Which clients have you done Q3-style procurement work for in the last twelve months?</li>
81-
<li>What's your typical engagement structure — fee-based, success-fee, retainer?</li>
82-
<li>Where do you draw the line between an additional and a non-additional credit on a forestry project?</li>
83-
<li>What's the smallest engagement Cairnstone is set up to take on?</li>
84-
<li>If we wanted to validate three credits we already hold, how would you scope that?</li>
85-
</ul>
86-
87-
<h2>Red flags &amp; sensitivities</h2>
88-
<p>Marcus's public posts on VCM quality have been pointed — he's named specific projects and registries. Avoid recommending or defending any project he's publicly criticised without being ready to back it up. Also: the warm-intro path may carry implicit obligation to the mutual contact — be aware of that before declining or escalating engagement.</p>
89-
</article>
62+
<article class="brief" id="brief-content"></article>
9063
</section>
9164
</main>
9265

0 commit comments

Comments
 (0)