←Back to projects

NorthLens

•Completed•Featured Project
TypeScriptNext.jsReactTailwind CSSGeminiSupabasepgvectorLeaflet
NorthLens

A GenAI participatory-planning prototype for Hong Kong's Northern Metropolis that answers residents' questions from cited official sources and turns their feedback into a dashboard for planners.

NorthLens

NorthLens 北覽 is a participatory-planning prototype I built for the Northern Metropolis, piloted on the Kwu Tung North New Development Area. A resident asks what the plan means for them and gets an answer where every claim is cited and tagged by how firm it is. Then they say what they think, by typing or by speaking, and a planner sees it grouped with everyone else's on a dashboard. The line I kept coming back to is that NorthLens doesn't ask AI to design Hong Kong. It uses AI to help Hong Kong design with its communities.

Project Overview

The app has two views. The resident view at / is where you pick a persona and a concern, ask a question in English or Traditional Chinese, and read the answer next to a map of the places it mentions. Below that are the Scenario Explorer and the Community Voice form. The planner view at /dashboard takes those submissions, anonymised and classified, and shows what people are worried about, who is worried, and where.

The knowledge base is small on purpose. It is 46 passages paraphrased from 23 public sources: Government press releases, Town Planning Board papers, LegCo replies, and MTR documents. Each passage carries a status. Completed, under construction, planned, proposal, under review, or superseded. That status follows the passage all the way to the screen.

It also runs with no keys at all. Without a Gemini key, retrieval falls back to keyword search and answers are composed from the retrieved passages by template. Without Supabase, feedback is written to a local JSON file. A status dot in the header shows which mode is live.

Motivation

Planning documents are public, but they are not readable. The answer to "can my mother get to the new station" is spread across an Outline Development Plan, a railway press release, and a LegCo reply, and some of those contradict each other because plans change. Kwu Tung North is a good example. The hospital that was originally planned there was superseded by a proposal in Ngau Tam Mei, and the site that was reserved is under review.

That is exactly where an ordinary chatbot is dangerous. It will repeat the old plan in a confident voice. So the rule for the build was that the model may only speak from passages it was handed, and a proposal must never read like a commitment.

The other half is the planner's side. A consultation produces hundreds of free-text submissions and somebody has to read them. I wanted the AI to do the sorting and a person to do the deciding. The AI proposes a category. A reviewer confirms it or corrects it. The original text is always kept.

Features

• Ask the plan
Pick one of three personas, a university student, a working adult, or a caregiver of an elderly parent, then pick a concern. The answer comes back in three parts: what may change, why it matters for you, and what is uncertain. Every claim has numbered citations and a status chip, so "Under construction" and "Proposal · may change" sit side by side in the same answer. Ask the plan

• Plan map
The map highlights the stations, facilities, and routes that the answer cites. It uses the HKSAR Lands Department basemap, and the labels switch language with the rest of the app. The Northern Link is drawn as a dashed line because it is not built yet. Plan map

• Scenario Explorer
A "living here" view that lines up everyday needs, transport, healthcare, elderly care, education, employment, green space, and cross-border access, against what the plans actually say. Each row links to its passages and shows their status. Healthcare is the row worth reading, because that is where the superseded hospital plan shows up. Scenario Explorer

• Community Voice
Tick the priorities that matter, then type or press Speak. Voice works in Cantonese, in English, and in a mix of both, so "天橋一定要有 lift" comes out the way it was said. Gemini transcribes the recording into the text box and the resident checks it before submitting. The audio is never stored. Names, phone numbers, HKID numbers, emails, and flat addresses are redacted before anything is classified or saved. Community Voice

• Planner dashboard
Filters for period, group, area, and live versus demo data. Above the fold are the headline numbers: concern rate, share from vulnerable groups, review coverage, how often reviewers agree with the AI's category, and voice share. An emerging concern banner names the issue and group with the most recent worry. Below that is a priority issues table with volume, sentiment, a 6-week trend, the 14-day change, and whether the related plan items are still open to change. Planner dashboard

• Issue drill-down, heatmap, and hotspots
Opening an issue shows suggested actions, the affected groups and areas, the related plan items with their sources, and quotes. A heatmap shows which groups worry about which issues, and a second map shows where the feedback clusters. Heatmap

• AI briefing
One button writes a short briefing for the planner: a headline, up to three findings, and up to three follow-ups. It is written from the computed statistics and nothing else. AI briefing

• Human review queue
Each submission shows the AI's proposed theme beside the original text. A reviewer confirms it or picks another from a dropdown. Reviewed rows are marked, and the original text is never changed. Review queue

• Exports
A consultation report and a feedback report, each with a print layout and a CSV download. Exports only ever contain responses a person has confirmed in the review queue. If nothing in the current filter has been confirmed, the buttons are disabled. Consultation report

• Traditional Chinese and guided tours
The 繁 toggle switches the whole flow, including the answers, the map labels, and the dashboard. Both views have a step-by-step tour that points at each section in turn. Traditional Chinese

Technical Architecture

Frontend

  • Next.js 16 App Router with React 19 and TypeScript
  • Tailwind CSS v4
  • Leaflet through react-leaflet, with the Lands Department basemap from the GeoData Store, which needs no API key

Retrieval

  • Each passage is embedded as one document holding both its English and its Chinese text, so a question in either language can find it
  • Three modes, in order: pgvector in Supabase through a match_kb_chunks RPC, in-memory Gemini embeddings, and keyword search
  • The keyword scorer is BM25-style, with stemming for English and bigrams for Chinese, plus a small synonym table so that 輪椅 or "difficulty walking" reaches the footbridge passages
  • The final ranking is hybrid: 60% semantic, 25% keyword, and 15% for topics that match the chosen persona and concern
  • A question that asks about uncertainty gives a small boost to passages that are proposed, under review, or superseded

Answering and guardrails

  • Gemini returns JSON against a response schema: a summary, three lists of claims, and an insufficient-evidence flag
  • Citations to passages that were not retrieved are dropped
  • A claim is dropped if it contains a number that does not appear in the passages it cites
  • A claim's status is the weakest status among its sources
  • If nothing survives, the answer says the evidence is thin and stops there

Feedback pipeline

  • PII redaction by pattern, before classification and before storage
  • Gemini classifies each submission into zone, theme, stakeholder, sentiment, and a suggested issue, with every field constrained to an enum
  • The result is validated again on the server. If a field is missing or outside the enum, the rule-based classifier is used instead
  • An empty feedback table seeds itself with 50 synthetic responses, flagged as synthetic in the data and in the UI

Insights

  • All dashboard statistics are computed in plain TypeScript in lib/insights.ts. No model is involved
  • The priority score is concerned responses × (1 + half the share from vulnerable groups) × a trend factor between 0.75 and 1.25 taken from the 14-day change

Storage

  • Supabase Postgres with RLS on and no public policies. The keys are server-only
  • Without Supabase, .data/feedback.json, written through a promise queue and a temp-file rename

Challenges and Solutions

Keeping a proposal from sounding like a promise was the main design problem. A prompt that says "be careful about uncertain plans" is a suggestion, and models ignore suggestions. So the status is not something the model writes. It comes from the passages. After the model answers, the server looks up every passage a claim cites and gives the claim the weakest status of the group. If a sentence leans on one station that is under construction and one footbridge that is only proposed, the chip says proposal. The model can phrase it however it likes. It cannot upgrade it.

Numbers were the other leak. Dates and distances are what residents will plan around, and they are what a model rounds or misremembers. Every number in a claim is pulled out and compared with the numbers in the cited passages, in both languages. One number that is not there and the whole claim is dropped. The AI briefing on the dashboard uses the same check. The server writes a block of facts from the computed statistics, the model writes prose from it, and any finding with a number that is not in that block is removed before the planner sees it. It is a blunt rule and it throws away some good sentences. I would rather lose a sentence than show a figure nobody can trace.

The model invents speech when given silence. Send a transcription model a recording of nothing and it does not return nothing. It returns a plausible sentence. For a tool that collects public opinion, that is a resident saying something they never said. There are three guards. The browser watches the microphone level while recording and counts the frames that actually contain sound, and a recording with too few never leaves the device. The server rejects clips that are too small to be real. And the response schema has a speech boolean, so the model has a way to say "nobody spoke" that is not an empty transcript. The transcript lands in the text box for the resident to check, never straight into the database.

Working with no keys shaped more of the code than I expected. Every AI step needed a second path that was not embarrassing. Retrieval has the keyword scorer. Chinese has no spaces, so the tokenizer splits runs of characters into bigrams, and a stop list removes the bigrams that turn up in every planning question. Answers have a template composer that only uses the first sentence of each retrieved passage, and its output goes through the same citation check as Gemini's. Classification has rules. The voice button and the briefing are simply hidden, because those have no honest fallback.

The local file store was easy to break. Two submissions arriving together could both read the file, both append, and one would overwrite the other. Writes now go through a single promise queue, and each write goes to a temp file that is renamed over the real one. A corrupt file is left alone and the request fails, because overwriting it with an empty list would quietly delete real feedback. There is a test for each of these: fifteen concurrent writes must all survive, a broken file must come out byte-for-byte the same, and an invalid review request must not touch the file at all.

Model output is input, and it gets validated like input. The classifier test feeds in a response with two extra properties, a replacement text and a replacement id, and checks that neither reaches the stored record. Only the six expected fields are copied across. If one of them is null, the rules take over.

Conclusion

NorthLens is a prototype, and a narrow one. It covers one development area, three personas, and 46 passages that I paraphrased by hand. That narrowness is why the guardrails work. Every passage has a status I checked, so the weakest-status rule has something true to stand on. I think the idea that carries over is the split. The model reads and phrases. The code decides what is allowed to be said, and a person decides what a piece of feedback means.

The weak point is freshness. Plans change, and right now a passage is only as current as the last time I edited the file. The next step is a freshness check: when a new press release or Town Planning Board paper comes in, flag the passages whose status may have moved, and have a person approve the change before residents see it. After that, voice for asking questions and not only for feedback, read-aloud answers, and a larger-text mode, because the people this is for are often the ones who find a text box hardest.

It is not an official source. Always check the latest Government release. The code is on GitHub, along with the use cases and a three-minute demo script.