Add introspection

This commit is contained in:
MartinBraquet
2025-08-02 16:20:22 +02:00
parent dd07b5ef82
commit e781fdce64
8 changed files with 222 additions and 181 deletions

View File

@@ -8,6 +8,8 @@ export async function GET(request: Request) {
const gender = url.searchParams.get("gender");
const minAge = url.searchParams.get("minAge");
const maxAge = url.searchParams.get("maxAge");
const minIntroversion = url.searchParams.get("minIntroversion");
const maxIntroversion = url.searchParams.get("maxIntroversion");
const interests = url.searchParams.get("interests")?.split(",").filter(Boolean) || [];
const causeAreas = url.searchParams.get("causeAreas")?.split(",").filter(Boolean) || [];
const connections = url.searchParams.get("connections")?.split(",").filter(Boolean) || [];
@@ -48,6 +50,22 @@ export async function GET(request: Request) {
}
}
// Add introversion filtering
if (minIntroversion || maxIntroversion) {
where.profile = {
...where.profile,
introversion: {}
};
if (minIntroversion) {
where.profile.introversion.gte = parseInt(minIntroversion);
}
if (maxIntroversion) {
where.profile.introversion.lte = parseInt(maxIntroversion);
}
}
// OR
// if (interests.length > 0) {
// where.profile = {

View File

@@ -26,6 +26,7 @@ function RegisterComponent() {
const [name, setName] = useState('');
const [gender, setGender] = useState('');
const [age, setAge] = useState<number | null>(null);
const [introversion, setIntroversion] = useState<number | null>(null);
const [personalityType, setPersonalityType] = useState('');
const [conflictStyle, setConflictStyle] = useState('');
const [image, setImage] = useState<string>('');
@@ -74,6 +75,7 @@ function RegisterComponent() {
setGender(profile.gender || '');
setPersonalityType(profile.personalityType || null);
setConflictStyle(profile.conflictStyle || '');
setIntroversion(profile.introversion || null);
if (profile.birthYear) {
setAge(new Date().getFullYear() - profile.birthYear);
}
@@ -315,6 +317,7 @@ function RegisterComponent() {
location,
gender: gender as Gender,
...(age && {birthYear: new Date().getFullYear() - age}),
introversion,
personalityType: personalityType as PersonalityType,
conflictStyle: conflictStyle as ConflictStyle,
images: keys,
@@ -592,49 +595,9 @@ function RegisterComponent() {
</div>
</div>
<div>
<label htmlFor="personalityType" className={headingStyle}>
Personality Type
</label>
<select
id="personalityType"
name="personalityType"
value={personalityType || ''}
onChange={(e) => setPersonalityType(e.target.value as PersonalityType)}
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
>
<option value="">Select your personality type</option>
{personalityOptions.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
<div>
<label htmlFor="conflictStyle" className={headingStyle}>
Conflict Style
</label>
<select
id="conflictStyle"
name="conflictStyle"
value={conflictStyle || ''}
onChange={(e) => setConflictStyle(e.target.value as ConflictStyle)}
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
>
<option value="">Select your conflict style</option>
{conflictOptions.map((style) => (
<option key={style} value={style}>
{style}
</option>
))}
</select>
</div>
<div className="relative" ref={dropdownRef}>
<label className={headingStyle}>
Interests
Core Interests
</label>
<div className="relative">
@@ -754,6 +717,63 @@ function RegisterComponent() {
</div>
</div>
<div>
<label htmlFor="introversion" className={headingStyle}>
Introversion (in %)
</label>
<input
id="introversion"
name="introversion"
type="number"
min="0"
max="100"
value={introversion ?? ''}
onChange={(e) => setIntroversion(Number(e.target.value))}
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
// placeholder=""
/>
</div>
{/*<div>*/}
{/* <label htmlFor="personalityType" className={headingStyle}>*/}
{/* Personality Type*/}
{/* </label>*/}
{/* <select*/}
{/* id="personalityType"*/}
{/* name="personalityType"*/}
{/* value={personalityType || ''}*/}
{/* onChange={(e) => setPersonalityType(e.target.value as PersonalityType)}*/}
{/* className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"*/}
{/* >*/}
{/* <option value="">Select your personality type</option>*/}
{/* {personalityOptions.map((type) => (*/}
{/* <option key={type} value={type}>*/}
{/* {type}*/}
{/* </option>*/}
{/* ))}*/}
{/* </select>*/}
{/*</div>*/}
<div>
<label htmlFor="conflictStyle" className={headingStyle}>
Conflict Style
</label>
<select
id="conflictStyle"
name="conflictStyle"
value={conflictStyle || ''}
onChange={(e) => setConflictStyle(e.target.value as ConflictStyle)}
className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
>
<option value="">Select your conflict style</option>
{conflictOptions.map((style) => (
<option key={style} value={style}>
{style}
</option>
))}
</select>
</div>
<div className="max-w-3xl w-full">
<label htmlFor="description" className={headingStyle}>
About You

View File

@@ -5,6 +5,7 @@ import {Gender} from "@prisma/client";
import Dropdown from "@/app/components/dropdown";
import Slider from '@mui/material/Slider';
import {DropdownKey, RangeKey} from "@/app/profiles/page";
import {capitalize} from "@/lib/format";
interface FilterProps {
filters: {
@@ -31,7 +32,8 @@ export const dropdownConfig: { id: DropdownKey, name: string }[] = [
]
export const rangeConfig: { id: RangeKey, name: string, min: number, max: number }[] = [
{id: "age", name: "Age Range", min: 15, max: 60},
{id: "age", name: "Age", min: 15, max: 60},
{id: "introversion", name: "Introversion (%)", min: 0, max: 100},
]
export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggleFilter, onReset}: FilterProps) {
@@ -212,18 +214,26 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
}
function getSlider(id: RangeKey, name: string, min: number, max: number) {
const minStr = 'min' + capitalize(id);
const maxStr = 'max' + capitalize(id);
const [minVal, setMinVal] = useState<number | undefined>(undefined);
const [maxVal, setMaxVal] = useState<number | undefined>(undefined);
return (
<div key={id + '.div'}>
<div className="w-full px-2">
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-2">{name}</label>
<Slider
value={[filters.minAge || min, filters.maxAge || max]}
onChange={(_, newValue) => {
let [_min, _max] = newValue as number[];
onFilterChange('minAge', (_min || min) > min ? _min : undefined);
onFilterChange('maxAge', (_max || max) < max ? _max : undefined);
value={[minVal || min, maxVal || max]}
onChange={(e, value) => {
let [_min, _max] = value;
setMinVal((_min || min) > min ? _min : undefined);
setMaxVal((_max || max) < max ? _max : undefined);
}}
onChangeCommitted={(e, value) => {
let [_min, _max] = value;
onFilterChange(minStr, (_min || min) > min ? _min : undefined);
onFilterChange(maxStr, (_max || max) < max ? _max : undefined);
}}
valueLabelDisplay="auto"
min={min}
@@ -245,8 +255,12 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
min={min}
max={max}
className="w-full p-2 border rounded-lg"
value={filters.minAge || ''}
onChange={(e) => onFilterChange('minAge', e.target.value ? parseInt(e.target.value) : undefined)}
value={minVal || ''}
onChange={(e) => {
const value = e.target.value ? parseInt(e.target.value) : undefined;
onFilterChange(minStr, value);
setMinVal(value);
}}
placeholder="Min"
/>
</div>
@@ -257,8 +271,12 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
min={min}
max={max}
className="w-full p-2 border rounded-lg"
value={filters.maxAge || ''}
onChange={(e) => onFilterChange('maxAge', e.target.value ? parseInt(e.target.value) : undefined)}
value={maxVal || ''}
onChange={(e) => {
const value = e.target.value ? parseInt(e.target.value) : undefined;
onFilterChange(maxStr, value);
setMaxVal(value);
}}
placeholder="Max"
/>
</div>
@@ -267,6 +285,8 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
)
}
const [text, setText] = useState<string>('');
return (
<div className="w-full mb-8">
<div className="flex flex-col sm:flex-row gap-4 mb-4">
@@ -277,8 +297,8 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
type="text"
placeholder="Search by name or description..."
className="w-full pl-10 pr-10 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
// value={filters.searchQuery}
// onChange={(e) => onFilterChange('searchQuery', e.target.value)}
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
const value = (e.target as HTMLInputElement).value;
@@ -288,7 +308,10 @@ export function ProfileFilters({filters, onFilterChange, onShowFilters, onToggle
/>
{filters.searchQuery && (
<button
onClick={() => onFilterChange('searchQuery', '')}
onClick={() => {
onFilterChange('searchQuery', '');
setText('');
}}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
type="button"
>

View File

@@ -4,7 +4,7 @@ import Link from "next/link";
import {useCallback, useEffect, useState} from "react";
import LoadingSpinner from "@/lib/client/LoadingSpinner";
import {ProfileData} from "@/lib/client/schema";
import {ProfileFilters, dropdownConfig} from "./ProfileFilters";
import {dropdownConfig, ProfileFilters} from "./ProfileFilters";
// Disable static generation
export const dynamic = "force-dynamic";
@@ -21,10 +21,11 @@ const initialState = {
causeAreas: [] as string[],
connections: [] as string[],
searchQuery: '',
forceRun: false,
};
export type DropdownKey = 'interests' | 'causeAreas' | 'connections';
export type RangeKey = 'age';
export type RangeKey = 'age' | 'introversion';
type OtherKey = 'gender' | 'searchQuery';
export default function ProfilePage() {
@@ -49,63 +50,61 @@ export default function ProfilePage() {
}, []); // <- runs once after initial mount
const fetchProfiles = useCallback(async () => {
try {
setLoading(true);
const params = new URLSearchParams();
try {
setLoading(true);
const params = new URLSearchParams();
if (filters.gender) params.append('gender', filters.gender);
if (filters.minAge) params.append('minAge', filters.minAge.toString());
if (filters.maxAge) params.append('maxAge', filters.maxAge.toString());
if (filters.gender) params.append('gender', filters.gender);
if (filters.minAge) params.append('minAge', filters.minAge.toString());
if (filters.maxAge) params.append('maxAge', filters.maxAge.toString());
if (filters.minIntroversion) params.append('minIntroversion', filters.minIntroversion.toString());
if (filters.maxIntroversion) params.append('maxIntroversion', filters.maxIntroversion.toString());
for (let i = 0; i < dropdownConfig.length; i++) {
const v = dropdownConfig[i];
const filterKey = v.id as DropdownKey;
if (filters[filterKey] && filters[filterKey].length > 0) {
params.append(v.id, filters[filterKey].join(','));
}
}
if (filters.searchQuery) params.append('search', filters.searchQuery);
const response = await fetch(`/api/profiles?${params.toString()}`);
const data = await response.json();
if (!response.ok) {
console.error(data.error || 'Failed to fetch profiles');
return;
}
setProfiles(data.profiles || []);
console.log(data.profiles);
if (renderImages) {
for (const u of data.profiles) {
console.log(u);
const img = u.image;
let url = img;
if (img && !img.startsWith('http')) {
const imageResponse = await fetch(`/api/download?key=${img}`);
console.log(`imageResponse: ${imageResponse}`)
if (imageResponse.ok) {
const imageBlob = await imageResponse.json();
url = imageBlob['url'];
}
}
setImages(prev => [...(prev || []), url]);
}
console.log(images);
}
} catch
(error) {
console.error('Error fetching profiles:', error);
} finally {
setLoading(false);
for (let i = 0; i < dropdownConfig.length; i++) {
const v = dropdownConfig[i];
const filterKey = v.id as DropdownKey;
if (filters[filterKey] && filters[filterKey].length > 0) {
params.append(v.id, filters[filterKey].join(','));
}
}
,
[filters]
)
;
if (filters.searchQuery) params.append('search', filters.searchQuery);
const response = await fetch(`/api/profiles?${params.toString()}`);
const data = await response.json();
if (!response.ok) {
console.error(data.error || 'Failed to fetch profiles');
return;
}
setProfiles(data.profiles || []);
console.log(data.profiles);
if (renderImages) {
for (const u of data.profiles) {
console.log(u);
const img = u.image;
let url = img;
if (img && !img.startsWith('http')) {
const imageResponse = await fetch(`/api/download?key=${img}`);
console.log(`imageResponse: ${imageResponse}`)
if (imageResponse.ok) {
const imageBlob = await imageResponse.json();
url = imageBlob['url'];
}
}
setImages(prev => [...(prev || []), url]);
}
console.log(images);
}
} catch
(error) {
console.error('Error fetching profiles:', error);
} finally {
setLoading(false);
}
}, [filters]);
useEffect(() => {
fetchProfiles();

View File

@@ -109,6 +109,40 @@ export function getProfile(url: string, header: any = null) {
console.log('userData', userData);
interface Tags {
profileAttribute: string;
attribute: string;
title: string;
}
const tagsConfig: Tags[] = [
{profileAttribute: 'desiredConnections', attribute: 'connection', title: 'Desired Connections'},
{profileAttribute: 'intellectualInterests', attribute: 'interest', title: 'Core Interests'},
{profileAttribute: 'causeAreas', attribute: 'causeArea', title: 'Cause Areas'},
]
function getTags({profileAttribute, attribute, title}: Tags) {
const values = userData?.profile?.[profileAttribute];
return <div key={profileAttribute + '.div'}>
{values?.length > 0 && (
<div className="mt-3"><
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> {title} </h2>
<div className="flex flex-wrap gap-2 mt-1">
{values.map((a: any) => (
<span
key={a?.[attribute]?.id}
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 dark:hover:bg-gray-500 transition"
>
{a?.[attribute]?.name}
</span>
))}
</div>
</div>
)
}
</div>;
}
return (
<article className="max-w-3xl mx-auto shadow-lg rounded-lg overflow-hidden">
{header}
@@ -226,74 +260,7 @@ export function getProfile(url: string, header: any = null) {
)
}
{userData?.profile?.desiredConnections?.length > 0 && (
<div className="mt-3"><
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Desired Connections </h2>
< ul
className="flex flex-wrap gap-2 mt-1">
{userData?.profile?.desiredConnections.map((value: any, idx: number) => (
<li
key={idx}
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 transition"
>
{value?.connection?.name
}
</li>
))
}
</ul>
</div>
)
}
{
userData?.profile?.intellectualInterests?.length > 0 && (
<div className="mt-3"><
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Interests </h2>
< ul
className="flex flex-wrap gap-2 mt-1">
{
userData.profile.intellectualInterests.map((value: any, idx: number) => (
<li
key={idx}
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 transition"
>
{value?.interest?.name
}
</li>
))
}
</ul>
</div>
)
}
{
userData?.profile?.causeAreas?.length > 0 && (
<div className="mt-3"><
h2 className="text-sm font-medium text-gray-500 uppercase tracking-wider"> Cause
Areas </h2>
< ul
className="flex flex-wrap gap-2 mt-1">
{
userData.profile.causeAreas.map((value: any, idx: number) => (
<li
key={idx}
className="px-3 py-1 text-sm bg-blue-100 text-blue-800 dark:text-white dark:bg-gray-700 rounded-full hover:bg-gray-200 transition"
>
{value?.causeArea?.name
}
</li>
))
}
</ul>
</div>
)
}
{tagsConfig.map((tag: any) => getTags(tag))}
{
userData?.profile?.description && (

View File

@@ -8,6 +8,7 @@ export interface ProfileData {
location: string;
gender: string;
birthYear: number;
introversion: number;
occupation: string;
personalityType: string;
conflictStyle: string;

View File

@@ -39,6 +39,7 @@ model Profile {
occupation String?
gender Gender?
personalityType PersonalityType?
introversion Int?
conflictStyle ConflictStyle?
images String[]
// communicationPreferences CommunicationPreferences[]

View File

@@ -10,6 +10,7 @@ async function main() {
type ProfileBio = {
name: string;
age: number;
introversion: number;
occupation: string;
location: string;
bio: string;
@@ -20,6 +21,7 @@ async function main() {
{
name: "Elena",
age: 29,
introversion: 75,
occupation: "Cognitive Science Researcher",
location: "Berlin, Germany",
bio: "Im passionate about understanding the limits and mechanics of human reasoning. I spend weekends dissecting papers on decision theory and evenings debating moral uncertainty. If you know your way around LessWrong and thought experiments, well get along.",
@@ -28,6 +30,7 @@ async function main() {
{
name: "Marcus",
age: 34,
introversion: 34,
occupation: "Software Engineer",
location: "San Francisco, USA",
bio: "Practicing instrumental rationality one well-calibrated belief at a time. Stoicism and startup life have taught me a lot about tradeoffs. Looking for someone who can argue in good faith and loves truth-seeking as much as I do.",
@@ -36,6 +39,7 @@ async function main() {
{
name: "Aya",
age: 26,
introversion: 56,
occupation: "Philosophy PhD Candidate",
location: "Oxford, UK",
bio: "My research focuses on metaethics and formal logic, but my heart belongs to moral philosophy. I think a lot about personhood, consciousness, and the ethics of future civilizations. Let's talk about Rawls or Parfit over tea.",
@@ -44,6 +48,7 @@ async function main() {
{
name: "David",
age: 41,
introversion: 71,
occupation: "Data Scientist",
location: "Toronto, Canada",
bio: "Former humanities major turned quant. Still fascinated by existential risk, the philosophy of science, and how to stay sane in an uncertain world. I'm here to meet people who think weird is a compliment.",
@@ -52,6 +57,7 @@ async function main() {
{
name: "Mei",
age: 31,
introversion: 12,
occupation: "Independent Writer",
location: "Singapore",
bio: "Writing essays on intellectual humility, the philosophy of language, and how thinking styles shape our lives. I appreciate calm reasoning, rigorous curiosity, and the beauty of well-defined concepts. Let's try to model each other's minds.",
@@ -67,7 +73,7 @@ async function main() {
});
});
console.log([...interests]);
console.log('Interests:', [...interests]);
// Create some interests and cause areas
await prisma.interest.createMany({
@@ -111,6 +117,7 @@ async function main() {
create: {
location: profile.location,
birthYear: 2025 - profile.age,
introversion: profile.introversion,
description: `[Dummy profile for demo purposes]\n${profile.bio}`,
gender: i % 2 === 0 ? 'Male' : 'Female',
personalityType: i % 3 === 0 ? 'Extrovert' : 'Introvert',
@@ -135,8 +142,13 @@ async function main() {
},
promptAnswers: {
create: [
{prompt: 'What motivates you?', answer: 'Curiosity and truth.'},
{prompt: 'How do you relate to your closest friends?', answer: 'By sharing our passions.'},
{prompt: 'Whats a belief youve changed your mind about after encountering strong evidence?', answer: 'I used to think willpower was the key to habit change. But research on environment design and cue-based behavior shifted my perspective. Now I optimize context, not just grit.'},
{prompt: 'What does thinking rationally mean to you in practice?', answer: 'It means being more committed to updating my beliefs than defending them — even if its uncomfortable. Especially when Im wrong.'},
{prompt: 'Whats a concept or topic that recently captivated you?', answer: 'Emergent complexity in ant colonies — how simple rules lead to intelligent systems. It made me rethink centralized control.'},
{prompt: 'If you could master any discipline outside your current field, what would it be and why?', answer: 'Philosophy of science. I love questioning the assumptions behind how we know what we know.'},
{prompt: 'What do you most value in a deep connection with someone?', answer: 'Shared intellectual honesty — the ability to explore truth collaboratively, without ego or defensiveness.'},
{prompt: 'When have you felt most deeply understood by someone?', answer: 'During a multi-hour conversation where we dissected a disagreement, not to win but to understand each others frameworks'},
{prompt: 'How do rationality and emotional closeness coexist for you?', answer: 'They reinforce each other. Being clear-headed helps me show up honestly and empathetically. I want to be close to people who care about truth — including emotional truth.'},
],
},
},