- Store amount_usd in Stripe webhook transaction (was only storing credits)
- Analytics endpoint: sum amount_usd for real USD revenue instead of credits
- Persona count: query personas collection directly (not credit transactions
which counted transactions, not individual personas — batch creation
of 3 at once appeared as count=1)
- Frontend: Revenue card now shows USD not credits; Gross Margin uses
USD revenue so calculation is dimensionally correct
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Per-persona message history: each persona now sees their own 8 previous
responses, preventing repetition and enabling position evolution
- OCEAN archetype labels in decision engine context: instead of raw numbers,
the decision LLM now sees "agreeableness: 72/100 [HIGH] — consensus-seeker"
- P2P interaction context: when participants interact directly, each one now
knows who they are responding to and what that person last said
- Python-level contrarian override: when agreement ratio in recent messages
exceeds 6% and a contrarian persona (low agreeableness or high neuroticism)
hasn't spoken recently, Python overrides moderator/probe action to call them
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add techSavviness, hasPurchasingPower, hasChildren, description to
_format_persona_details() — previously ignored despite being in persona model
- Rewrite OCEAN block: each trait now includes its psychological definition,
full name, and [LOW/MODERATE/HIGH] label so LLM understands the scoring scale
(e.g. "Agreeableness (cooperation, empathy, conflict avoidance): 25/100 [LOW]")
- Add extraversion behavioral constraint to _generate_behavioral_instructions()
(was used for response length only, never for personality guidance)
- Add LOW conscientiousness constraint (spontaneous, gut-feel)
- Add BALANCED PERSONALITY baseline for personas with all-moderate OCEAN scores —
previously these got zero behavioral constraints, behaving generically
- Add optimist/enthusiast personality keyword mapping
- Add OCEAN archetype legend to conversation-decision-engine.md so decision
engine understands what high/low scores mean when selecting next speaker
- Add +15% Diversity Boost modifier: prefer speakers whose archetype differs
from the last 2 participants (reduces echo-chamber dynamics)
- Add OCEAN model explanation header to focus-group-response.md prompt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extend _format_persona_details() to include background, traits, values,
interests, communication_style, brandLoyalty, priceConsciousness, demographics
(race/ethnicity/culture) — previously these fields were stored but never
sent to the LLM, causing all personas to give generic homogeneous answers
- Add _generate_behavioral_instructions() that translates OCEAN scores into
explicit behavioral directives (low agreeableness → must push back,
high neuroticism → voice of doubt, skeptic keyword → find the overpromise)
- Update focus-group-response.md to render BEHAVIORAL CONSTRAINTS and
DEMOGRAPHIC CONTEXT sections so LLM uses cultural/financial context
- Add anti-groupthink rule to conversation-decision-engine: on convergence,
prefer a contrarian persona before asking moderator probe question
- Improve ai-moderator-system.md: active @mention pull-in of silent
participants, spontaneous follow-up probes on interesting responses,
engagement tracking
- Add LANGUAGE RULE to all three response prompts: always match the language
of the question (fixes mixed Russian/English responses)
- Add POST /focus-groups/<id>/restart endpoint: clears session collections,
resets status to 'new', preserves config
- Add POST /focus-groups/<id>/duplicate endpoint: copies config to new group
- Frontend: Duplicate + Restart buttons on focus group cards with confirmation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
dict(session.metadata) fails with KeyError:0 — metadata is a StripeObject
in SDK v5, not a plain dict. Use getattr() for all metadata fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
stripe.Webhook.construct_event() now returns StripeObject instances that
do not support .get()/__getitem__ — must use attribute access.
event["type"] → event.type, session.get("x") → session.x, etc.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useMyUsage (and all other queries keyed without user identity) were showing
cached data from the previous user after logout/re-login. queryClient.clear()
in clearAuthData() wipes all in-memory cache on sign-out.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /api/usage/me now returns credits_balance, credits_spent (MTD debits), and
last 30 credit transactions; no dollar amounts or token counts exposed
- MyUsage page redesigned: balance + MTD spent cards, transaction history table,
Buy Credits button
- Admin: POST /api/billing/test-checkout — $1 Stripe Checkout for any credit
amount to validate the payment flow end-to-end
- stripe_service: cast unit_amount to int (Stripe rejects float)
- i18n updated in EN/RU/UK
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _get_runtime_config(): reads active provider endpoint, api_key, main/mini
model from app_settings (60s cache), falls back to env vars
- get_azure_client() now async, accepts cfg dict
- All generate_* methods call _get_runtime_config() per invocation so DB
changes take effect without restart
- app_settings: _seed_from_env() backfills empty endpoint/api_key from env
vars on first load so the admin UI shows current values immediately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Batch-resolves user_id strings to emails after the aggregation via a
single find() on the users collection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PDF via fpdf2 (DejaVu font from fonts-dejavu-core, Cyrillic support)
- Lang param (?lang=ru/en) passed from i18n.language in ThemesPanel
- Prompts updated: executive summary + new key-decisions prompt, both
write entirely in the requested language
- Analytics section: participation bar chart per participant
- Key themes with quotes rendered as side-bordered blocks
- Filename: ASCII slug for Content-Disposition + UTF-8 filename* fallback
- api.ts: handles PDF blob, decodes UTF-8 filename*, 120s timeout
- AnalyticsPanel, ThemeHighlighter: light-mode colors → dark tokens
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FocusGroupSession.tsx: replace bg-slate-50 with bg-background so the page
matches the rest of the dark UI; swap remaining text-slate-* for muted tokens.
focus_group_ai.py: before deducting 40 cr for an AI mode run, check if this
focus group already has a charge within the last 4 hours. Skip deduction if
found — prevents double-billing when the server restarts mid-session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AppLayout: add user info + 'My account' + 'Sign out' to mobile hamburger menu
- Header: add 'Sign out' button to authenticated mobile menu (landing page)
- dist/og-image.png: sync with public/ (Cohorta-branded, replaces Lovable image)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously Register.tsx wrote auth_token directly to localStorage without
calling setToken/setUser in AuthContext. This made isAuthenticated=true
(computed from localStorage) while user=null (React state), so ProtectedRoute's
user.email_verified check was never reached.
- AuthContext: add setAuth(token, userData) that syncs both localStorage and React state
- Register: use setAuth() instead of direct localStorage write
- Register: add !registered guard to auto-redirect effect to prevent bypassing check-inbox screen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without .dockerignore, COPY . . was overwriting the npm ci install
with the server's stale node_modules, causing Rollup to pick up the
wrong @radix-ui/react-slot version.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes Docker build failure: react-progress brought react-primitive@2.1.4
which needs createSlot from react-slot >=1.2.0. npm overrides ensure
single 1.2.4 copy is used everywhere, no nested conflicts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
npm ci in Docker resolved react-progress's nested react-primitive which
requires createSlot from react-slot >=1.2.0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /api/focus-groups/{id}/report/download generates a markdown report
with AI-written executive summary, key themes with quotes, and full
transcript. Frontend adds "Export Full Report" button to ThemesPanel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All copy updated: "Session name", "What are you researching?", "Key topics to explore",
"Session length", "Thinking depth", "Response length", "Attach materials (optional)"
- llm_model, reasoning_effort, verbosity moved into collapsible "Advanced settings" accordion
- Word-count indicators now float inside textareas (absolute overlay) instead of below
- Removed duplicate amber text blocks and redundant FormDescriptions
- FocusGroupModerator header: "AI Focus Group Moderator" → "Set Up Your Research Session"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Epic 1 — OG-image & SEO base:
- Replace wrong og-image.png with branded 1200×630 Cohorta design
- index.html: full title, og:type/url/image dimensions, twitter:card, canonical
Epic 2 — Pricing from admin panel:
- Pricing.tsx: remove hardcoded DEFAULT_PACKS; add loading skeleton and error+retry state
- Features list and personas/sessions counts computed from API credits/costs
- billing.py /packs: also returns persona_cost and run_cost for frontend math
- app_settings.py: add popular:True to pro pack default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Header: replace banner PNG with Logo SVG mark (44px) + Cohorta wordmark — crisp at all sizes
- ScrollToTop: moved from PublicLayout to App.tsx root — works on all pages,
avoids position:fixed breakage from framer-motion transform ancestors
- ScrollToTop: threshold 100px → appears sooner after scroll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>