lovegpt / backend /src /services /profileService.ts
Crownelius's picture
Upload OpenDateBase Gradio Space
c536472 verified
Raw
History Blame Contribute Delete
5.74 kB
import { supabaseAdmin } from "../config/supabase";
import type { ProfileAnswer } from "../types/domain";
import { HttpError } from "../utils/httpError";
import { getQuestionsDocument, isKnownQuestionId } from "../utils/questions";
import { createEmbedding } from "./embeddingService";
import { summarizeProfile } from "./openClawService";
interface UpsertProfileInput {
displayName: string;
age: number;
location?: string;
gender?: string;
interestedIn?: string[];
}
function answerText(answer: ProfileAnswer): string {
const raw = Array.isArray(answer.answer) ? answer.answer.join(", ") : answer.answer;
return [raw, answer.followup].filter(Boolean).join(" ");
}
function normalizeSignal(value: string): string {
return value
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, " ")
.replace(/\s+/g, " ")
.trim()
.slice(0, 80);
}
function collectSignals(answers: Record<string, ProfileAnswer>, tag: "disgust" | "captivating_traits"): string[] {
const questions = getQuestionsDocument().questions.filter((question) => question.tags.includes(tag));
const signals = new Set<string>();
for (const question of questions) {
const answer = answers[question.id];
if (!answer) {
continue;
}
const values = Array.isArray(answer.answer)
? answer.answer
: answer.answer.split(/[.;,\n]/).map((item) => item.trim());
for (const value of values) {
const normalized = normalizeSignal(value);
if (normalized.length >= 3) {
signals.add(normalized);
}
}
}
return [...signals].slice(0, 32);
}
function profileTextForEmbedding(answers: Record<string, ProfileAnswer>, narrativeSummary: string): string {
const questions = getQuestionsDocument().questions;
const lines = questions
.map((question) => {
const answer = answers[question.id];
return answer ? `${question.prompt}\n${answerText(answer)}` : "";
})
.filter(Boolean);
return [narrativeSummary, ...lines].join("\n\n");
}
export async function getMyProfile(userId: string) {
const { data, error } = await supabaseAdmin
.from("profiles")
.select("*")
.eq("user_id", userId)
.maybeSingle();
if (error) {
throw error;
}
return data;
}
export async function upsertMyProfile(userId: string, input: UpsertProfileInput) {
const { data, error } = await supabaseAdmin
.from("profiles")
.upsert(
{
user_id: userId,
display_name: input.displayName,
age: input.age,
location: input.location ?? null,
gender: input.gender ?? null,
interested_in: input.interestedIn ?? []
},
{ onConflict: "user_id" }
)
.select("*")
.single();
if (error) {
throw error;
}
return data;
}
export async function saveProfileAnswer(userId: string, questionId: string, input: Omit<ProfileAnswer, "questionId" | "updatedAt">) {
if (!isKnownQuestionId(questionId)) {
throw new HttpError(400, "unknown_question", `Unknown question id: ${questionId}`);
}
const profile = await getMyProfile(userId);
if (!profile) {
throw new HttpError(409, "profile_required", "Create the basic profile before saving answers");
}
const answers = (profile.answers ?? {}) as Record<string, ProfileAnswer>;
answers[questionId] = {
questionId,
answer: input.answer,
followup: input.followup,
dealbreakerSeverity: input.dealbreakerSeverity,
updatedAt: new Date().toISOString()
};
const { data, error } = await supabaseAdmin
.from("profiles")
.update({ answers, profile_complete: false })
.eq("user_id", userId)
.select("*")
.single();
if (error) {
throw error;
}
return data;
}
export async function completeMyProfile(userId: string) {
const profile = await getMyProfile(userId);
if (!profile) {
throw new HttpError(409, "profile_required", "Create the basic profile before completing the profile");
}
const answers = (profile.answers ?? {}) as Record<string, ProfileAnswer>;
const requiredQuestionIds = getQuestionsDocument().questions.map((question) => question.id);
const missing = requiredQuestionIds.filter((questionId) => !answers[questionId]);
if (missing.length > 0) {
throw new HttpError(422, "profile_incomplete", `Missing required answers: ${missing.join(", ")}`);
}
const narrativeSummary = await summarizeProfile(answers);
const embedding = await createEmbedding(profileTextForEmbedding(answers, narrativeSummary));
const disgustTriggers = collectSignals(answers, "disgust");
const captivatingTraits = collectSignals(answers, "captivating_traits");
const { data, error } = await supabaseAdmin
.from("profiles")
.update({
narrative_summary: narrativeSummary,
embedding,
disgust_triggers: disgustTriggers,
captivating_traits: captivatingTraits,
profile_complete: true
})
.eq("user_id", userId)
.select("*")
.single();
if (error) {
throw error;
}
return data;
}
export async function markUserPaid(userId: string, stripeCustomerId?: string | null, sessionId?: string | null) {
const paidAt = new Date().toISOString();
const { error: entitlementError } = await supabaseAdmin
.from("user_entitlements")
.upsert(
{
user_id: userId,
entry_paid: true,
stripe_customer_id: stripeCustomerId,
stripe_checkout_session_id: sessionId,
paid_at: paidAt
},
{ onConflict: "user_id" }
);
if (entitlementError) {
throw entitlementError;
}
const { error: profileError } = await supabaseAdmin
.from("profiles")
.update({ paid_entry: true })
.eq("user_id", userId);
if (profileError) {
throw profileError;
}
}