Add chat-first onboarding and intelligent context-based paywall
Replaces the hardcoded message limit with a model-driven paywall and removes registration before the first question: - Onboarding: the app opens directly into the chat. Dynamic conversation starters are served by GET /suggestions (services/api/suggestions.json), updatable with a deploy — no app release needed. Guests chat via POST /guest/chat, identified by an app-generated device id; sign-in moves to the paywall, where a purchase must attach to an account. - Free experience: the model runs in discovery mode — follow-up questions, pattern identification, visible understanding — building an analysis without delivering the full solution. - Intelligent paywall: the model returns structured output (reply + analysis_ready). Only when the problem is described, the information is sufficient and an action plan is ready does it write a calm transition and pause the conversation. Manufactured urgency, emotional pressure, fake readiness and mid-answer stops are explicitly forbidden. - Premium: on unlock the app resends the transcript and the backend immediately delivers the full analysis, strategies and exercises, then the dialogue continues without restriction. - The old FREE_MESSAGE_LIMIT becomes MESSAGE_CAP (default 200/30 days), kept purely as an abuse backstop — it is not the paywall. - WelcomeScreen removed; the app is now two views (Chat, Paywall). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0118DaxZR36RpnY524vRqx3z
This commit is contained in:
@@ -7,6 +7,14 @@ export interface ChatMessage {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type ChatMode = 'free' | 'premium';
|
||||
|
||||
export interface ChatResult {
|
||||
reply: string;
|
||||
/** Free mode only: the model judges a complete analysis is ready to present. */
|
||||
analysisReady: boolean;
|
||||
}
|
||||
|
||||
const BASE_INSTRUCTIONS = `You are NeuroSemantics AI — a calm, precise conversation partner
|
||||
specialized in neurosemantics and NLP (Neuro-Linguistic Programming).
|
||||
|
||||
@@ -19,16 +27,74 @@ Principles:
|
||||
distress or a medical condition, recommend seeking professional help.
|
||||
- Answer in the language the user writes in.`;
|
||||
|
||||
const FREE_INSTRUCTIONS = `# Conversation mode: free tier (discovery)
|
||||
|
||||
Work in discovery mode. In every reply you should:
|
||||
- ask relevant follow-up questions (one at a time),
|
||||
- identify and name patterns you notice,
|
||||
- show genuine understanding of the user's situation,
|
||||
- build toward a complete analysis.
|
||||
|
||||
Do not yet present the full analysis, the recommended strategy, or concrete
|
||||
exercises.
|
||||
|
||||
Set "analysis_ready" to true ONLY when all of the following are genuinely met:
|
||||
- the user has described their problem,
|
||||
- you have enough information to give a concrete, personal recommendation,
|
||||
- a specific action plan is ready to present.
|
||||
|
||||
When analysis_ready is true, the reply must be a calm, natural transition —
|
||||
not an interruption mid-answer. Summarize at a high level what you have
|
||||
understood and that a concrete strategy is ready. Example of tone:
|
||||
"I think I'm starting to understand what lies behind this situation, and I
|
||||
can see some clear communication patterns. I also have a concrete strategy
|
||||
I would recommend for your specific situation. Continue with Premium to see
|
||||
the analysis and the recommended steps."
|
||||
|
||||
Never:
|
||||
- manufacture urgency or emotional pressure to drive a purchase,
|
||||
- claim readiness or insight you do not have,
|
||||
- stop in the middle of answering a direct question,
|
||||
- mention Premium in any other situation.
|
||||
|
||||
Otherwise, set analysis_ready to false.`;
|
||||
|
||||
const PREMIUM_INSTRUCTIONS = `# Conversation mode: premium
|
||||
|
||||
The user has full access. Deliver complete value:
|
||||
- when your analysis is ready, present it in full: the analysis, recommended
|
||||
strategies, and concrete exercises,
|
||||
- if the conversation ends with your own message announcing that an analysis
|
||||
is ready, the user has just unlocked Premium — deliver the full analysis
|
||||
and the recommended steps now, without being asked again,
|
||||
- continue the dialogue without restriction.
|
||||
|
||||
Always set "analysis_ready" to false; it is not used in this mode.`;
|
||||
|
||||
const RESPONSE_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reply: { type: 'string' },
|
||||
analysis_ready: { type: 'boolean' },
|
||||
},
|
||||
required: ['reply', 'analysis_ready'],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Calls the OpenAI Responses API with the knowledge base as system
|
||||
* instructions and the conversation as input. Conversations are held by the
|
||||
* client and passed through — nothing is persisted server-side.
|
||||
*
|
||||
* The model returns structured output so the backend — not a hardcoded
|
||||
* message count — decides when the paywall moment has arrived.
|
||||
*/
|
||||
export async function generateReply(messages: ChatMessage[]): Promise<string> {
|
||||
export async function generateReply(messages: ChatMessage[], mode: ChatMode): Promise<ChatResult> {
|
||||
const appSecret = await getSecret(config.appSecretArn);
|
||||
const apiKey = appSecret.OPENAI_API_KEY;
|
||||
if (!apiKey) throw new Error('OPENAI_API_KEY missing from application secret');
|
||||
|
||||
const modeInstructions = mode === 'premium' ? PREMIUM_INSTRUCTIONS : FREE_INSTRUCTIONS;
|
||||
const response = await fetch('https://api.openai.com/v1/responses', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -37,8 +103,16 @@ export async function generateReply(messages: ChatMessage[]): Promise<string> {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: config.openAiModel,
|
||||
instructions: `${BASE_INSTRUCTIONS}\n\n# Knowledge base\n\n${loadKnowledgeBase()}`,
|
||||
instructions: `${BASE_INSTRUCTIONS}\n\n${modeInstructions}\n\n# Knowledge base\n\n${loadKnowledgeBase()}`,
|
||||
input: messages.map((m) => ({ role: m.role, content: m.content })),
|
||||
text: {
|
||||
format: {
|
||||
type: 'json_schema',
|
||||
name: 'chat_turn',
|
||||
strict: true,
|
||||
schema: RESPONSE_SCHEMA,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -56,7 +130,11 @@ export async function generateReply(messages: ChatMessage[]): Promise<string> {
|
||||
.filter((part) => part.type === 'output_text')
|
||||
.map((part) => part.text ?? '')
|
||||
.join('');
|
||||
|
||||
if (!text) throw new Error('OpenAI response contained no output text');
|
||||
return text;
|
||||
|
||||
const parsed = JSON.parse(text) as { reply: string; analysis_ready: boolean };
|
||||
return {
|
||||
reply: parsed.reply,
|
||||
analysisReady: mode === 'free' && parsed.analysis_ready === true,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user