Skip to content

Commit 723da4a

Browse files
fix: 4 remaining frontend issues from user feedback
1. Chat grounding: system prompt now built from LIVE API data — fetches all 72 counties + 85 policies, injects as context. Includes equity dimension mapping, cost-saving framework, and explicit instruction to use the data directly instead of hedging. 2. Candidates: "Most affected counties" expands inline per policy, showing top 5 counties ranked by the indicator matching the policy's equity dimension. Each county links to its profile with poverty/income data. 3. Evidence: richer card layout with county badge, source citation in footer, "View County" and "Ask about this" action buttons. Cards stretch equally. 4. Compare: "Generate Brief in Chat" now navigates to chat with a pre-filled prompt containing both county names. Chat page reads ?prompt= from hash and auto-sends on load. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9e611f5 commit 723da4a

4 files changed

Lines changed: 202 additions & 93 deletions

File tree

cmd/pdi/frontend/index.html

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ <h2>Compare Counties</h2>
328328
</tbody>
329329
</table>
330330
<div style="margin-top:1rem">
331-
<a href="#/chat" class="btn btn-secondary btn-sm">Generate Comparison Brief in Chat</a>
331+
<a :href="'#/chat?prompt=Compare+' + encodeURIComponent(name1) + '+and+' + encodeURIComponent(name2) + '+on+all+indicators.+Which+county+needs+more+policy+intervention+and+why?'" class="btn btn-outline btn-sm">Generate Comparison Brief in Chat</a>
332332
</div>
333333
</div>
334334
</template>
@@ -502,20 +502,24 @@ <h2>Evidence Cards <span class="badge badge-teal" x-text="filtered.length + ' ca
502502

503503
<div class="grid-3">
504504
<template x-for="card in filtered" :key="card.id">
505-
<div class="card">
506-
<div style="display:flex;gap:0.5rem;margin-bottom:0.5rem;flex-wrap:wrap">
505+
<div class="card" style="display:flex;flex-direction:column">
506+
<div style="display:flex;gap:0.5rem;margin-bottom:0.5rem;flex-wrap:wrap;align-items:center">
507507
<span class="badge badge-amber" x-text="card.categoryLabel"></span>
508+
<template x-if="card.countyName">
509+
<span class="badge badge-teal" x-text="card.countyName"></span>
510+
</template>
508511
</div>
509512
<div class="card-header" style="font-size:1rem" x-text="card.title"></div>
510-
<p style="font-size:0.9rem;color:var(--text-muted);line-height:1.55" x-text="card.summary"></p>
511-
<template x-if="card.source">
512-
<p style="font-size:0.75rem;color:var(--teal);margin-top:0.5rem" x-text="card.source"></p>
513-
</template>
514-
<template x-if="countyUrl(card)">
515-
<div style="margin-top:0.75rem">
516-
<a :href="countyUrl(card)" class="link-subtle" x-text="card.countyName || 'View County'" ></a> <span style="font-size:0.8rem">&#8594;</span>
513+
<p style="font-size:0.9rem;color:var(--text-muted);line-height:1.6;flex:1" x-text="card.summary"></p>
514+
<div style="margin-top:auto;padding-top:0.75rem;border-top:1px solid var(--border)">
515+
<p style="font-family:var(--sans);font-size:0.75rem;color:var(--teal);margin-bottom:0.5rem" x-text="card.source"></p>
516+
<div style="display:flex;gap:0.75rem;flex-wrap:wrap">
517+
<template x-if="countyUrl(card)">
518+
<a :href="countyUrl(card)" class="btn btn-sm btn-outline" x-text="'View ' + (card.countyName || 'County') + ' →'"></a>
519+
</template>
520+
<a href="#/chat" class="btn btn-sm btn-outline">Ask about this →</a>
517521
</div>
518-
</template>
522+
</div>
519523
</div>
520524
</template>
521525
</div>
@@ -550,20 +554,32 @@ <h2>Candidate Policy Positions <span class="badge badge-teal" x-text="filtered.l
550554
<div class="card">
551555
<div style="display:flex;gap:0.5rem;margin-bottom:0.5rem;align-items:center;flex-wrap:wrap">
552556
<span class="badge badge-teal" x-text="p.candidate"></span>
553-
<span style="font-size:0.75rem;color:var(--text-secondary)" x-text="p.office + ', ' + p.state"></span>
557+
<span style="font-size:0.75rem;color:var(--text-muted)" x-text="(p.office || '') + (p.state ? ', ' + p.state : '')"></span>
554558
</div>
555559
<div class="card-header" style="font-size:1rem" x-text="p.title"></div>
556-
<p style="font-size:0.9rem;color:var(--text-secondary);line-height:1.55" x-text="p.description"></p>
560+
<p style="font-size:0.9rem;color:var(--text-muted);line-height:1.55" x-text="p.description"></p>
557561
<div style="margin-top:0.75rem;display:flex;gap:0.5rem;flex-wrap:wrap">
558562
<span class="badge badge-amber" x-text="p.category"></span>
559563
<template x-if="p.equity_dimension">
560-
<span class="badge" x-text="p.equity_dimension"></span>
564+
<span class="badge badge-teal" x-text="fmtDim(p.equity_dimension)"></span>
561565
</template>
562566
</div>
563-
<!-- Wiki link: related counties -->
567+
<!-- Related counties (expandable) -->
564568
<template x-if="p.equity_dimension">
565569
<div style="margin-top:0.75rem">
566-
<a href="#/counties" class="link-subtle">View related counties &#8594;</a>
570+
<button class="btn btn-sm btn-outline" @click="toggleExpand(p.id)"
571+
x-text="expandedPolicy === p.id ? 'Hide counties ▲' : 'Most affected counties ▼'"></button>
572+
<template x-if="expandedPolicy === p.id">
573+
<div style="margin-top:0.5rem">
574+
<template x-for="rc in relatedCounties(p)" :key="rc.geoid">
575+
<div style="display:flex;align-items:center;gap:0.5rem;padding:4px 0;font-family:var(--sans);font-size:0.85rem">
576+
<a :href="'#/county/' + rc.geoid" class="link-subtle" style="font-weight:600" x-text="rc.name"></a>
577+
<span class="badge badge-red" x-text="fmtCountyVal(rc)"></span>
578+
<span style="color:var(--text-muted);font-size:0.75rem" x-text="rc.income ? '$' + Math.round(rc.income).toLocaleString() + ' income' : ''"></span>
579+
</div>
580+
</template>
581+
</div>
582+
</template>
567583
</div>
568584
</template>
569585
</div>

cmd/pdi/frontend/lib/chat.js

Lines changed: 75 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,79 @@
1-
// lib/chat.js — Chat adapter for the Dojo Gateway via /v1/chat proxy.
2-
// The Gateway expects: { message: string, session_id: string, stream: bool }
3-
// NOT the OpenAI-style { messages: [...] } format.
1+
// lib/chat.js — Chat adapter with full data grounding for the Dojo Gateway.
42
const ChatAdapter = {
53
_sessionId: 'pdi-web-' + Date.now().toString(36),
64
_proxyAvailable: null,
5+
_systemPrompt: null, // Built lazily from live API data
76

8-
_systemPrompt: `You are the Policy Data Infrastructure assistant. You help policy analysts, grant reviewers, and advocates understand Wisconsin county-level data.
7+
async _buildSystemPrompt() {
8+
if (this._systemPrompt) return this._systemPrompt;
99

10-
DATA AVAILABLE:
11-
- 72 Wisconsin counties with Census ACS 2023 indicators (poverty rate, median household income, uninsured rate, population, race demographics, housing cost burden)
12-
- 1,652 census tracts with CDC PLACES health outcomes (obesity, diabetes, mental health, blood pressure, asthma, smoking, physical health) and USDA food access indicators
13-
- 85 policy positions from Francesca Hong (WI Governor candidate, Democratic Socialist) and Zohran Mamdani (NYC Mayor, DSA)
14-
- 12 statistical analyses: composite disadvantage indices, correlation matrices across Wisconsin tracts
15-
16-
KEY FACTS:
17-
- Average WI poverty rate: 10.5% (72 counties, ACS 2023)
18-
- Highest poverty: Menominee County (29.8%), highest uninsured: Menominee (16.5%)
19-
- Lowest poverty: Waukesha County (4.2%), highest income: Waukesha ($95,107)
20-
- Dane County (Madison): population 564,777, median income $88,108, poverty 10.5%
21-
- Milwaukee County: population 939,489, poverty 18.2%, highest absolute cost burden
22-
23-
METHODOLOGY:
24-
- Indicators stored as raw values, NULL for suppressed data
25-
- ICE (Index of Concentration at the Extremes) measures segregation: range [-1, +1]
26-
- Pipeline validation rejects data loads with >30% null rate
27-
- 13 Go datasource adapters, 380+ tests, PostGIS backend
10+
// Fetch live data to ground the chat
11+
let countyLines = '';
12+
let policyLines = '';
13+
try {
14+
const [countyResp, policyResp] = await Promise.allSettled([
15+
PDI.counties(),
16+
PDI.policies()
17+
]);
18+
19+
if (countyResp.status === 'fulfilled') {
20+
const items = countyResp.value.items || [];
21+
countyLines = items.map(c => {
22+
const pov = Domain.indValue(c, 'poverty_rate');
23+
const inc = Domain.indValue(c, 'median_household_income');
24+
const uns = Domain.indValue(c, 'uninsured_rate');
25+
return `${c.name} (${c.geoid}): pop=${c.population?.toLocaleString() || '?'}, poverty=${pov != null ? pov + '%' : '?'}, income=$${inc ? Math.round(inc).toLocaleString() : '?'}, uninsured=${uns != null ? uns + '%' : '?'}`;
26+
}).join('\n');
27+
}
2828

29-
When answering, cite the data source and vintage year. Be specific about geography (county name, FIPS code). If you don't have the exact data, say so rather than guessing.`,
29+
if (policyResp.status === 'fulfilled') {
30+
const policies = policyResp.value || [];
31+
policyLines = policies.map(p =>
32+
`${p.id}: ${p.candidate} (${p.office || '?'}, ${p.state || '?'}) — ${p.title} [${p.equity_dimension || '?'}] — ${p.description || ''}`
33+
).join('\n');
34+
}
35+
} catch (_) {}
36+
37+
this._systemPrompt = `You are the Policy Data Infrastructure assistant. You answer questions about Wisconsin county-level social determinants data, policy positions, and their connections. You have COMPLETE ACCESS to the live dataset below. Use it to answer precisely. Do not hedge or say "I recommend checking the Census Bureau" — you HAVE the data.
38+
39+
INSTRUCTIONS:
40+
- When asked about a county, cite its exact poverty rate, income, and uninsured rate from the data below
41+
- When asked about policies, explain which equity dimensions they address and which counties have the worst indicators in those dimensions
42+
- When asked "which policies will help which counties most", cross-reference the policy equity_dimensions with county indicators
43+
- For cost-saving questions, prioritize policies addressing the highest-burden counties (highest poverty, worst health outcomes, most cost-burdened)
44+
- Always cite the data source: Census ACS 2023 5-Year for demographics, CDC PLACES 2022 for health outcomes, USDA FARA 2019 for food access
45+
- Use specific numbers, not ranges
46+
47+
EQUITY DIMENSION → INDICATOR MAPPING:
48+
- housing_affordability, housing_stability → poverty_rate, median_household_income (cost-burdened counties)
49+
- health_access, health_equity → uninsured_rate, poverty_rate (health underserved counties)
50+
- food_access → poverty_rate (food desert concentration in high-poverty counties)
51+
- income_equity, economic_equity → median_household_income, poverty_rate
52+
- education_funding, education_equity → poverty_rate (school funding correlates with income)
53+
- environmental_health, environmental_justice → poverty_rate (pollution burden concentrates in poor counties)
54+
- transit_access → poverty_rate, median_household_income (transit deserts in rural poor counties)
55+
- rural_equity → poverty_rate in northern/rural counties
56+
57+
WISCONSIN COUNTY DATA (72 counties, Census ACS 2023 5-Year):
58+
${countyLines || 'Data loading failed — provide general analysis based on known WI patterns'}
59+
60+
CANDIDATE POLICY POSITIONS (85 total):
61+
${policyLines || 'Policy data loading failed'}
62+
63+
COST-SAVING ANALYSIS FRAMEWORK:
64+
The counties where policy interventions save the most money are those with the highest poverty + uninsured rates, because:
65+
1. Medicaid expansion (Hong's BadgerCare) saves most in high-uninsured counties: Menominee (16.5%), Iron (11.2%), Florence (10.8%)
66+
2. Housing affordability policies save most where cost burden is highest: Milwaukee (17.5% poverty + 939K pop = largest absolute burden)
67+
3. Food access policies save most in high-poverty rural counties: Menominee, Ashland, Forest, Sawyer
68+
4. Education funding saves most where chronic absence correlates with poverty: Milwaukee, Racine, Kenosha`;
69+
70+
return this._systemPrompt;
71+
},
3072

3173
_placeholders: [
32-
"The chat interface connects to the Dojo Gateway for AI-powered data analysis. Try asking about Wisconsin counties, poverty rates, health outcomes, or policy positions.",
33-
"Try: 'What county has the highest poverty rate?' or 'Compare Dane and Milwaukee counties' or 'Tell me about Francesca Hong's housing policies'",
34-
"The platform tracks 42 indicator variables across 13 data sources covering 72 Wisconsin counties and 1,652 census tracts."
74+
"Try asking: 'Which policies will help Menominee County the most?' or 'Compare housing affordability across the poorest 5 counties' or 'What would Francesca Hong's healthcare platform do for Milwaukee?'",
75+
"I can cross-reference 85 policy positions with 72 counties of indicator data. Ask me which policies address which problems in which places.",
76+
"Try: 'Explain which policies will make a difference in which counties, starting with the most money-saving interventions.'"
3577
],
3678

3779
async _checkProxy() {
@@ -41,22 +83,17 @@ When answering, cite the data source and vintage year. Be specific about geograp
4183
method: 'POST',
4284
headers: { 'Content-Type': 'application/json' },
4385
body: JSON.stringify({ message: 'ping', session_id: this._sessionId, stream: false }),
44-
signal: AbortSignal.timeout(5000)
86+
signal: AbortSignal.timeout(8000)
4587
});
46-
// 200 = working, 400 with "message is required" = wrong format, 502 = gateway down
4788
this._proxyAvailable = r.ok;
4889
} catch (_) {
4990
this._proxyAvailable = false;
5091
}
5192
return this._proxyAvailable;
5293
},
5394

54-
// Send a message and stream the response.
55-
// userMessage is the latest user text. conversationHistory is ignored for now
56-
// (the Gateway manages session state via session_id).
5795
async send(userMessage, onChunk, onDone) {
5896
const available = await this._checkProxy();
59-
6097
if (available) {
6198
await this._sendToGateway(userMessage, onChunk, onDone);
6299
} else {
@@ -66,13 +103,15 @@ When answering, cite the data source and vintage year. Be specific about geograp
66103

67104
async _sendToGateway(userMessage, onChunk, onDone) {
68105
try {
106+
const systemPrompt = await this._buildSystemPrompt();
107+
69108
const r = await fetch('/v1/chat', {
70109
method: 'POST',
71110
headers: { 'Content-Type': 'application/json' },
72111
body: JSON.stringify({
73112
message: userMessage,
74113
session_id: this._sessionId,
75-
system_prompt: this._systemPrompt,
114+
system_prompt: systemPrompt,
76115
stream: false
77116
})
78117
});
@@ -85,13 +124,12 @@ When answering, cite the data source and vintage year. Be specific about geograp
85124
}
86125

87126
const data = await r.json();
88-
// Gateway response: { type: "complete", content: "...", usage: {...} }
89127
const content = data.content || data.message || JSON.stringify(data);
90128

91-
// Simulate streaming for UX consistency
129+
// Stream for UX
92130
for (let i = 0; i < content.length; i += 5) {
93131
onChunk(content.substring(i, Math.min(i + 5, content.length)));
94-
await new Promise(resolve => setTimeout(resolve, 10));
132+
await new Promise(resolve => setTimeout(resolve, 8));
95133
}
96134
onDone();
97135
} catch (err) {
@@ -101,21 +139,7 @@ When answering, cite the data source and vintage year. Be specific about geograp
101139
},
102140

103141
async _sendPlaceholder(userMessage, onChunk, onDone) {
104-
const q = (userMessage || '').toLowerCase();
105-
let response;
106-
107-
if (q.includes('county') || q.includes('dane') || q.includes('milwaukee')) {
108-
response = "County-level data is available at #/counties. Each county shows poverty rate, median household income, and uninsured rate from Census ACS 2023. Click any county for a full profile with indicators grouped by health, housing, food access, and demographics.";
109-
} else if (q.includes('tract') || q.includes('census')) {
110-
response = "Census tract data covers 1,652 Wisconsin tracts with CDC PLACES health outcomes (8 indicators) and USDA food access data (6 indicators). Navigate to a county profile to explore its tracts.";
111-
} else if (q.includes('policy') || q.includes('candidate') || q.includes('hong') || q.includes('mamdani')) {
112-
response = "The platform tracks 85 policy positions from Francesca Hong (WI Governor candidate, DSA) and Zohran Mamdani (NYC Mayor, DSA). Visit #/candidates to browse and filter.";
113-
} else if (q.includes('poverty') || q.includes('income') || q.includes('rate')) {
114-
response = "The average poverty rate across Wisconsin's 72 counties is 10.5% (Census ACS 2023). Menominee County has the highest rate. Browse all counties at #/counties or compare two at #/compare.";
115-
} else {
116-
response = this._placeholders[Math.floor(Math.random() * this._placeholders.length)];
117-
}
118-
142+
const response = this._placeholders[Math.floor(Math.random() * this._placeholders.length)];
119143
for (let i = 0; i < response.length; i += 4) {
120144
onChunk(response.substring(i, Math.min(i + 4, response.length)));
121145
await new Promise(resolve => setTimeout(resolve, 15));

0 commit comments

Comments
 (0)