Refactor profile and form submission logic to support partial updates and improve type handling

This commit is contained in:
MartinBraquet
2026-03-31 13:41:24 +02:00
parent a7ae62a14b
commit a31765b7ae
4 changed files with 44 additions and 29 deletions

View File

@@ -46,7 +46,7 @@ import {RadioToggleGroup} from 'web/components/widgets/radio-toggle-group'
import {Select} from 'web/components/widgets/select'
import {Slider} from 'web/components/widgets/slider'
import {Title} from 'web/components/widgets/title'
import {useChoicesContext} from 'web/hooks/use-choices'
import {ChoiceMap, ChoiceSetter, useChoicesContext} from 'web/hooks/use-choices'
import {api} from 'web/lib/api'
import {useLocale, useT} from 'web/lib/locale'
import {track} from 'web/lib/service/analytics'
@@ -60,7 +60,7 @@ export const OptionalProfileUserForm = (props: {
user: BaseUser
buttonLabel?: string
bottomNavBarVisible?: boolean
onSubmit: () => Promise<void>
onSubmit: (profile?: ProfileWithoutUser) => Promise<void>
}) => {
const {profile, user, buttonLabel, setProfile, onSubmit, bottomNavBarVisible = true} = props
@@ -90,11 +90,11 @@ export const OptionalProfileUserForm = (props: {
const [isExtracting, setIsExtracting] = useState(false)
const [parsingEditor, setParsingEditor] = useState<any>(null)
const handleLLMExtract = async () => {
const handleLLMExtract = async (): Promise<Partial<ProfileWithoutUser>> => {
const llmContent = parsingEditor?.getText?.() ?? ''
if (!llmContent) {
toast.error(t('profile.llm.extract.error_empty', 'Please enter content to extract from'))
return
return {}
}
setIsExtracting(true)
const isInputUrl = isUrl(llmContent)
@@ -103,11 +103,13 @@ export const OptionalProfileUserForm = (props: {
...(isInputUrl ? {url: urlize(llmContent).trim()} : {content: llmContent.trim()}),
}
try {
const extracted = await api('llm-extract-profile', payload)
for (const data of Object.entries(removeNullOrUndefinedProps(extracted))) {
const key = data[0]
let extractedProfile = await api('llm-extract-profile', payload)
extractedProfile = removeNullOrUndefinedProps(extractedProfile)
for (const data of Object.entries(extractedProfile)) {
const key = data[0] as keyof ProfileWithoutUser
let value = data[1]
let choices, setChoices: any
let choices: ChoiceMap | undefined
let setChoices: ChoiceSetter | undefined
if (key === 'interests') {
choices = interestChoices
setChoices = setInterestChoices
@@ -133,19 +135,26 @@ export const OptionalProfileUserForm = (props: {
}
debug({value, converter})
} else if (key === 'keywords') setKeywordsString((value as string[]).join(', '))
setProfile(
key as keyof ProfileWithoutUser,
value as ProfileWithoutUser[keyof ProfileWithoutUser],
)
;(extractedProfile as Record<string, unknown>)[key] = value
}
if (!isInputUrl) extractedProfile.bio = parsingEditor?.getJSON?.()
debug({
text: parsingEditor?.getText?.(),
json: parsingEditor?.getJSON?.(),
extracted: extractedProfile,
})
for (const key of Object.keys(extractedProfile) as (keyof ProfileWithoutUser)[]) {
setProfile(key, extractedProfile[key] as ProfileWithoutUser[typeof key])
}
if (!isInputUrl) setProfile('bio', parsingEditor?.getJSON?.())
debug({text: parsingEditor?.getText?.(), json: parsingEditor?.getJSON?.(), extracted})
parsingEditor?.commands?.clearContent?.()
toast.success(
t('profile.llm.extract.success', 'Profile data extracted! Please review below.'),
)
return extractedProfile
} catch (error) {
console.error(error)
toast.error(
@@ -162,6 +171,7 @@ export const OptionalProfileUserForm = (props: {
} finally {
setIsExtracting(false)
}
return {}
}
const errorToast = () => {
@@ -169,19 +179,22 @@ export const OptionalProfileUserForm = (props: {
}
const handleSubmit = async () => {
let finalProfile = profile
if (parsingEditor?.getText?.()?.trim()) {
await handleLLMExtract()
const extractedProfile = await handleLLMExtract()
finalProfile = {...profile, ...extractedProfile}
}
// Validate age before submitting
if (typeof profile['age'] === 'number') {
if (profile['age'] < 18) {
if (typeof finalProfile['age'] === 'number') {
if (finalProfile['age'] < 18) {
setAgeError(t('profile.optional.age.error_min', 'You must be at least 18 years old'))
setIsSubmitting(false)
errorToast()
return
}
if (profile['age'] > 100) {
if (finalProfile['age'] > 100) {
setAgeError(t('profile.optional.age.error_max', 'Please enter a valid age'))
setIsSubmitting(false)
errorToast()
@@ -193,7 +206,7 @@ export const OptionalProfileUserForm = (props: {
track('submit optional profile')
await onSubmit()
await onSubmit(finalProfile)
choices.refreshInterests()
choices.refreshCauses()

View File

@@ -70,10 +70,13 @@ const useChoices = (label: OptionTableKey) => {
return {choices, refreshChoices}
}
export type ChoiceMap = Record<string, string>
export type ChoiceSetter = React.Dispatch<React.SetStateAction<ChoiceMap>>
export type UseAllChoices = {
interests: Record<string, string>
causes: Record<string, string>
work: Record<string, string>
interests: ChoiceMap
causes: ChoiceMap
work: ChoiceMap
refreshInterests: () => void
refreshCauses: () => void
refreshWork: () => void

View File

@@ -73,8 +73,8 @@ function ProfilePageInner(props: {user: User; profile: Profile}) {
setBaseUser((prevState) => ({...prevState, [key]: value}))
}
async function submitForm() {
const {interests, causes, work, ...otherProfileProps} = profile
async function submitForm(finalProfile?: ProfileWithoutUser) {
const {interests, causes, work, ...otherProfileProps} = finalProfile ?? profile
const parsedProfile = removeUndefinedProps(otherProfileProps) as any
debug('parsedProfile', parsedProfile)
const promises: Promise<any>[] = filterDefined([
@@ -124,7 +124,7 @@ function ProfilePageInner(props: {user: User; profile: Profile}) {
setProfile={setProfileState}
user={baseUser}
buttonLabel={t('profile.save', 'Save')}
onSubmit={async () => await submitForm()}
onSubmit={submitForm}
/>
</Col>
</Col>

View File

@@ -16,8 +16,7 @@ import {CompassLoadingIndicator} from 'web/components/widgets/loading-indicator'
import {useTracking} from 'web/hooks/use-tracking'
import {api} from 'web/lib/api'
import {auth, CACHED_REFERRAL_USERNAME_KEY} from 'web/lib/firebase/users'
import {useLocale} from 'web/lib/locale'
import {useT} from 'web/lib/locale'
import {useLocale, useT} from 'web/lib/locale'
import {getLocale} from 'web/lib/locale-cookie'
import {track} from 'web/lib/service/analytics'
import {safeLocalStorage} from 'web/lib/util/local'
@@ -75,7 +74,7 @@ export default function SignupPage() {
scrollTo(0, 0)
}
const handleFinalSubmit = async () => {
const handleFinalSubmit = async (finalProfile?: ProfileWithoutUser) => {
setIsSubmitting(true)
const referredByUsername = safeLocalStorage
? (safeLocalStorage.getItem(CACHED_REFERRAL_USERNAME_KEY) ?? undefined)
@@ -85,7 +84,7 @@ export default function SignupPage() {
const deviceToken = ensureDeviceToken()
try {
const profile = removeNullOrUndefinedProps({
...profileForm,
...(finalProfile ?? profileForm),
referred_by_username: referredByUsername,
}) as any
const {interests, causes, work, ...otherProfileProps} = profile