| name | questions-answers |
| description | Q&A system in Raamattu Nyt — full pipeline: search-query classification, question upsert, AI
answer generation, community answers, voting, admin moderation, anchor/similar questions, and
info-palaset (info blocks). Also covers Vieraskynä / guest authors (account-less named
attribution: guest_authors, GuestAuthorSelect, get_author_public_info), anonymous-submission
bot protection (verify-human / Turnstile / honeypot / throttle), AI-answer UI hidden from
non-admins (aiVisibility), and the info-block library (AdminInfoBlocksPage, InfoBlockEditor,
info_blocks + question/topic/path junctions, rendered via InfoTextCard / InfoView /
InfoCinema).
|
Questions & Answers System
Full-stack Q&A pipeline: search → classify → upsert → answer (AI/community/admin) → moderate → publish.
Architecture Overview
User search query
→ questionClassifier.ts (client-side)
→ upsert-question (edge fn: normalize, hash, classify, deduplicate)
→ QuestionsSearchSection renders:
├── Answers grouped by type (bullet → editorial → community → AI)
├── Info-palaset (info blocks attached to the question — see below)
├── CommunityAnswerForm (auth-gated)
├── QuestionConsentBlock (AI generation / human request)
├── AnchorQuestionsSection (topic-linked curated Q&A)
└── SimilarQuestionsSection (trigram similarity)
Key Files
Edge Functions
| Function | Path | Purpose |
|---|
upsert-question | supabase/functions/upsert-question/ | Normalize, classify, upsert, find similar |
generate-ai-answer | supabase/functions/generate-ai-answer/ | AI answer via Claude (paid, quota-checked) |
submit-community-answer | supabase/functions/submit-community-answer/ | User-submitted answer (free, needs admin approval); bot-gated |
submit-feedback | supabase/functions/submit-feedback/ | Anonymous app feedback (bot-gated; replaces the old direct anon insert) |
vote | supabase/functions/vote/ | +1/-1 voting on questions/answers |
All three anon-callable submit functions (upsert-question, submit-community-answer,
submit-feedback) run _shared/verify-human.ts (honeypot + Turnstile + throttle) first.
Hooks (apps/raamattu-nyt/src/hooks/)
| Hook | File | Purpose |
|---|
useUpsertQuestion | useQuestions.ts | Mutation: upsert question via edge fn |
useGenerateAiAnswer | useQuestions.ts | Mutation: generate AI answer (quota-checked) |
useSubmitCommunityAnswer | useQuestions.ts | Mutation: submit community answer |
useVote | useQuestions.ts | Mutation: vote on question/answer |
usePublishedQuestion | useQuestions.ts | Query: single published question by slug |
useQuestionAnswers | useQuestions.ts | Query: published answers for question |
useAdminQuestions | useQuestions.ts | Query: all questions with answer stats |
useAdminAnswers | useQuestions.ts | Query: all answers (including unpublished) |
useAnchorQuestions | useAnchorQuestions.ts | Query: anchor Q&A grouped by topic |
Components (apps/raamattu-nyt/src/components/search/)
| Component | Purpose |
|---|
QuestionsSearchSection | Main Q&A display in search results |
QuestionConsentBlock | AI answer / human request consent |
CommunityAnswerForm | User answer submission form |
SimilarQuestionsSection | Trigram-similar questions |
AnchorQuestionsSection | Topic-linked curated Q&A |
Pages
| Page | Route | Purpose |
|---|
QuestionPage.tsx | /kysymys/:slug | Public single question view + admin tools |
AdminQuestionsPage.tsx | /ohjaamo/questions | Full admin Q&A dashboard |
PremiumQuestionsPage.tsx | /premium/questions | Public questions browsing |
AdminGuestAuthorsPage.tsx | /ohjaamo/guest-authors | CRUD for Vieraskynä (guest) authors |
Core Libraries
| File | Purpose |
|---|
src/lib/questionClassifier.ts | Client-side question detection (Finnish) |
Database Schema (bible_schema)
For full schema details: See references/schema.md
Tables
- questions — Normalized questions with classification, status, voting
- answers — Answers with author_type, rich content, key_verses, topics, strongs_refs
- votes — User votes (+1/-1) on questions/answers (unique per user)
- topic_anchor_questions — Links questions as anchor Q&A for topics
Answer Author Types
| Type | Created by | Published by default? |
|---|
bullet | Admin | Yes |
admin | Admin | Yes |
system | Import/seed | Yes |
ai | Edge function | No (needs admin review) |
community | Authenticated user | No (needs admin approval) |
guest | Admin (Vieraskynä) | Yes — attributed to a guest_authors row via guest_author_id |
Author display: account authors (author_id) resolve their public name via the
get_author_public_info RPC (profiles RLS is admin-or-self); guest authors (guest_author_id)
show display_name/@username from guest_authors. See Vieraskynä.
Question Status Flow
draft → needs_review → approved → published
└→ rejected
RLS Summary
- Anon/Auth: Read published questions + published non-hidden answers
- Auth: Read own questions + own community answers, insert votes, submit community answers
- Admin: Full CRUD on all questions, answers, votes
Common Patterns
Add new answer type
- Add CHECK constraint value in migration
- Update
AnswerData interface in QuestionsSearchSection.tsx
- Update
Answer type in useQuestions.ts
- Add rendering logic in
renderAnswerCard
- Update admin management in
AdminQuestionsPage.tsx
Add new question classification
- Update classifier patterns in
questionClassifier.ts
- Mirror logic in
upsert-question edge function
- Update CHECK constraint if new DB value needed
- Add UI handling in
QuestionsSearchSection
Modify AI answer generation
- Edit prompt template via
ai-prompt-manager skill
- Edge function:
generate-ai-answer/index.ts
- Output validation: excerpt, key_verses, topics, strongs_refs, related_questions
- Model/vendor configured via
ai_feature_bindings table
Add feature to community answers
- Edge function:
submit-community-answer/index.ts
- Hook:
useSubmitCommunityAnswer in useQuestions.ts
- Form:
CommunityAnswerForm.tsx (search) or inline in QuestionPage.tsx
- Admin tab: "Yhteisövastaukset" in
AdminQuestionsPage.tsx
Vieraskynä (guest authors)
Account-less named attribution for answers AND info-palat. An admin can credit a
piece to a bible_schema.guest_authors row (a named contributor with no login) via
guest_author_id + author_type='guest', instead of a real account.
| Concern | File |
|---|
| List/CRUD hook | hooks/useGuestAuthors.ts (RPC get_guest_authors for active list; bibleSchemaTable("guest_authors") for admin CRUD) |
| Admin picker | components/admin/GuestAuthorSelect.tsx (used in NewAdminAnswerForm, InfoBlockEditor) |
| Admin page | pages/AdminGuestAuthorsPage.tsx (/ohjaamo/guest-authors) |
| Public name resolve | hooks/useUsernamesByIds.ts → RPC get_author_public_info(uuid[]) |
| Render | question-detail/AnswerCard.tsx, question-detail/InfoTextCard.tsx |
Gotcha: account authors' public names come from get_author_public_info (SECURITY
DEFINER) because public.profiles SELECT is admin-or-self — a direct profiles read returns
nothing to guests, so named authors would silently not render. The old account-only
AuthorSelect.tsx + lib/designatedAuthors.ts were removed — don't reference them.
Anonymous-submission bot protection
Anon submit paths (upsert-question, submit-community-answer, submit-feedback) gate
on supabase/functions/_shared/verify-human.ts: honeypot (empty hidden field + ≥1500 ms
on screen), Cloudflare Turnstile (verified via TURNSTILE_SECRET_KEY; skipped when the
secret is unset, so dev/staging just work), and a rate-limit (check_and_record_throttle
→ public.submission_throttle, service_role-only). Client widget/fields:
components/security/HumanCheckFields.tsx + useHumanCheck.ts (wired into
QuestionConsentBlock, AskQuestionButton, AppFeedbackForm). When adding a new anon
submit path, add the fields client-side and call verifyHuman(...) server-side.
AI answers hidden from users
AI answer generation still exists (ai answer type, generate-ai-answer) but its UI is
hidden from non-admin users — lib/aiVisibility.ts#shouldHideAIForUser(plan) returns true
for everyone except admin (AI quotas are zeroed for all user plans). Gate any AI
button/panel/token display behind it. Exception: plan-from-summary (kooste→lukusuunnitelma)
is NOT gated — it stays visible for Pro/Premium.
Info-palaset (info blocks)
Reusable content blocks (bible_schema.info_blocks) attached to questions, topics, and
spiritual paths. One of two answer mechanisms for a question (alongside community
answers). Admin-curated — no AI generation and no classifier (unlike questions).
[!important] Key invariant
An info-pala is rendered by TWO components: InfoTextCard (page-tila) and InfoView
(cinema-tila). Edit whichever the request targets and keep both in sync.
Tables (bible_schema)
- info_blocks —
slug, title_fi/en, body_md/en, body_html/en, excerpt/en,
is_published, is_handled, is_hidden, is_core (gates attachability), author_type
(admin/community/system), icon, bg_color, image_url, key_verses (display-only),
key_prayers, action_links (route/url/grand_plan; legacy action_grand_plan_id).
- question_info_blocks / topic_info_blocks / spiritual_path_info_blocks — junctions
(
sort_order; paths also carry binding_id).
RPCs
get_info_blocks_by_slugs(p_slugs) — public, published blocks by slug
get_attachable_info_blocks() — admin-only (SECURITY DEFINER), core blocks + attach counts
get_spiritual_path_info_block_slugs[_admin]() — path bindings
Key files
| Concern | File |
|---|
| CRUD hook | hooks/useInfoBlocks.ts |
| Fetch by slug | hooks/info-blocks/useInfoBlocksBySlugs.ts |
| Question-attached | hooks/questions/useQuestionInfoBlocks.ts (+ admin variant) |
| Topic-attached | hooks/topics/useTopicInfoBlocks.ts |
| Path bindings | hooks/info-blocks/useSpiritualPathInfoBlock{Bindings,Mutations}.ts |
| Page render (1 block) | pages/question-detail/InfoTextCard.tsx (wrapper QuestionInfoSection.tsx) |
| Cinema render | components/cinema/InfoView.tsx (1) · InfoFlow.tsx (snap-scroll N) · InfoCinema.tsx (shell) |
| Attach dialog | pages/question-detail/AddInfoBlockDialog.tsx · components/info-blocks/InfoBlockAttachDialog.tsx |
| Action links | components/info-blocks/InfoBlockActions.tsx |
| Path render | components/spiritual-path/SpiritualPathInfoBlock.tsx |
| Admin | pages/AdminInfoBlocksPage.tsx (/ohjaamo/info-blocks) · pages/admin-info-blocks/InfoBlockEditor.tsx |
Q&A cinema integration
QuestionCinemaApp renders published community answers (via answerToInfoBlock()) +
attached info blocks through the same InfoFlow/InfoView — answers first, then info blocks.
Common patterns
- Create/edit an info-pala →
InfoBlockEditor (fill both FI + EN; is_core gates attachability).
- Attach to question/topic/path → attach dialog → junction row with
sort_order.
- Change appearance → page =
InfoTextCard, cinema = InfoView — edit both.
- Info blocks are read via RPC + typed
bibleSchemaRpc (not direct .from("info_blocks")) → mock those in tests.
Full info-block schema + RLS: See references/schema.md.
Shared UI Components
- BibleReferencePickerPopover — Verse picker popover (used across 6 files)
- SafeTextarea — Input with XSS protection, markup detection
- RichContentRenderer — Renders body_html from rich editor
- VerseContentCard — Displays verse reference with content
- FeedbackThumbs / FeedbackText — Answer feedback (AI answers)
References