@@ -1,11 +1,13 @@
"use client" ;
import "@/app/site-styles" ;
import Link from "next/link" ;
import dynamic from "next/dynamic" ;
import { useRouter } from "next/navigation" ;
import { ArrowDown , ArrowUpRight , Sparkles , X } from "lucide-react" ;
import { ArrowDown , ArrowUpRight , Sparkles } from "lucide-react" ;
import { useEffect , useRef , useState } from "react" ;
import type { FormEvent , KeyboardEvent } from "react" ;
import { AccountDialogOverlay , type AccountOverlayModel } from "@/components/account-dialog-overlay" ;
import { AppSidebar } from "@/components/app-sidebar" ;
import { UserAvatar } from "@/components/user-avatar" ;
import {
@@ -25,12 +27,12 @@ import {
} from "@/lib/rectification-entry" ;
import { ConversationalBirthTimeRectification , type PersistedRectificationTurn } from "@/components/conversational-birth-time-rectification" ;
import { ChatMessageContent } from "@/components/chat-message-content" ;
import { AgentAvatar , ChatMessageRow } from "@/components/chat-message-row" ;
import { AgentAvatar } from "@/components/chat-message-row" ;
import {
ChatMessageActions ,
toggleChatMessageFeedback ,
type ChatMessageFeedback ,
} from "@/components/chat-message-actions" ;
import { ChatTranscript , type ChatTranscriptActions } from "@/components/chat-transcript" ;
import { ModelSelector } from "@/components/model-selector" ;
import { OnboardingRedeemPaywall } from "@/components/onboarding-redeem-paywall" ;
import { BirthPlacePicker } from "@/components/birth-place-picker" ;
@@ -39,9 +41,7 @@ import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/s
import { composerDraftSnapshot , setComposerDraft } from "@/lib/composer-draft" ;
import { clearStaleClientReload } from "@/lib/stale-client-recovery" ;
import { chinaLocations , type ProvinceNode } from "@/data/china-locations" ;
import { ConversationFollowUps } from "@/components/conversation-follow-ups" ;
import { parseAgentReply , isGenericSessionTitle , resolveSessionTitle , type ReplyTheme } from "@/lib/agent-reply" ;
import { deriveConsultationFollowUps } from "@/lib/consultation-follow-ups" ;
import {
beamAvatarPalettes ,
beamAvatarSchema ,
@@ -100,7 +100,8 @@ import {
BALANCE_CHANGED_EVENT ,
membershipHref ,
} from "@/lib/membership" ;
import { chatMessageViews , nextActivityView , activityCompletedTrail , type AgentActivityView , type ChatMessage } from "@/lib/chat-message-view" ;
import { nextActivityView , activityCompletedTrail , type AgentActivityView , type ChatMessage } from "@/lib/chat-message-view" ;
import { chartLibrarySessionBranch } from "@/lib/chart-library-session" ;
import {
emptyConsultationTimeline ,
reduceConsultationTimeline ,
@@ -1094,7 +1095,6 @@ export default function Home() {
const [ accountMenuOpen , setAccountMenuOpen ] = useState ( false ) ;
const [ activeAccountDialog , setActiveAccountDialog ] = useState < AccountDialog | null > ( null ) ;
const [ chartLibrary , setChartLibrary ] = useState < ChartLibraryRecord [ ] > ( [ ] ) ;
const [ chartLibraryOpen , setChartLibraryOpen ] = useState ( false ) ;
const [ synastryRelationshipType , setSynastryRelationshipType ] = useState < SynastryRelationshipType > ( "romance" ) ;
const [ synastryPendingId , setSynastryPendingId ] = useState < string | null > ( null ) ;
const [ otherProfileDraft , setOtherProfileDraft ] = useState < Profile > ( emptyProfile ) ;
@@ -1178,6 +1178,15 @@ export default function Home() {
const modelSelectionVersions = useRef ( new Map < string , number > ( ) ) ;
const activeSessionIdRef = useRef ( "" ) ;
const chartLibraryLoadedAccount = useRef ( "" ) ;
const openChartLibraryOnDialog = useRef ( false ) ;
const accountOverlayRef = useRef < AccountOverlayModel | null > ( null ) ;
const accountOverlayEpochRef = useRef ( 0 ) ;
const chatActionsRef = useRef < ChatTranscriptActions > ( {
onFeedback() { } ,
onCopy() { } ,
onRegenerate() { } ,
onFollowUp() { } ,
} ) ;
const activeOnboardingRequestIdentity = useRef ( "" ) ;
const accountRefreshGuard = useRef ( createLatestAccountRequestGuard ( ) ) ;
const resumeRectificationSession = useRef < ( session : ChatSession ) = > void > ( ( ) = > undefined ) ;
@@ -1300,43 +1309,41 @@ export default function Home() {
} , [ accountId , archivedSessionIds , hydrated , pinnedSessionIds ] ) ;
useEffect ( ( ) = > {
if ( ! accountId ) {
const branch = chartLibrarySessionBranch ( accountId , chartLibraryLoadedAccount . current ) ;
if ( branch === "clear" || ! accountId ) {
setChartLibrary ( [ ] ) ;
setSynastryHistory ( [ ] ) ;
chartLibraryLoadedAccount . current = "" ;
return ;
}
if ( chartLibraryLoadedAccount . current === accountId ) return ;
chartLibraryLoadedAccount . current = accountId ;
setChartLibrary ( upsertSelfChart ( readChartLibrary ( accountId ) , profile ) ) ;
setSynastryHistory ( readSynastryHistory ( accountId ) ) ;
void fetchCloudChartLibrary ( )
. then ( ( cloudLibrary ) = > {
setChartLibrary ( ( ) = > {
const next = upsertSelfChart ( cloudLibrary . filter ( ( record ) = > record . role !== "self" ) , profile ) ;
localStorage . setItem ( chartLibraryStorageKey ( accountId ) , JSON . stringify ( next ) ) ;
return next ;
if ( branch === "hydrate-then-persist" ) {
chartLibraryLoadedAccount . current = accountId ;
setChartLibrary ( upsertSelfChart ( readChartLibrary ( accountId ) , profile ) ) ;
setSynastryHistory ( readSynastryHistory ( accountId ) ) ;
void fetchCloudChartLibrary ( )
. then ( ( cloudLibrary ) = > {
setChartLibrary ( ( ) = > {
const next = upsertSelfChart ( cloudLibrary . filter ( ( record ) = > record . role !== "self" ) , profile ) ;
localStorage . setItem ( chartLibraryStorageKey ( accountId ) , JSON . stringify ( next ) ) ;
return next ;
} ) ;
} )
. catch ( ( ) = > {
// Cloud chart library is best-effort; local library remains usable.
} ) ;
} )
. catch ( ( ) = > {
// Cloud chart library is best-effort; local library remains usable.
} ) ;
void fetchCloudSynastryHistory ( )
. then ( ( cloudHistory ) = > {
setSynastryHistory ( ( current ) = > {
const byId = new Map ( [ . . . current , . . . cloudHistory ] . map ( ( record ) = > [ record . id , record ] as const ) ) ;
const next = [ . . . byId . values ( ) ] . sort ( ( a , b ) = > b . createdAt - a . createdAt ) . slice ( 0 , 10 ) ;
writeSynastryHistory ( accountId , next ) ;
return next ;
void fetchCloudSynastryHistory ( )
. then ( ( cloudHistory ) = > {
setSynastryHistory ( ( current ) = > {
const byId = new Map ( [ . . . current , . . . cloudHistory ] . map ( ( record ) = > [ record . id , record ] as const ) ) ;
const next = [ . . . byId . values ( ) ] . sort ( ( a , b ) = > b . createdAt - a . createdAt ) . slice ( 0 , 10 ) ;
writeSynastryHistory ( accountId , next ) ;
return next ;
} ) ;
} )
. catch ( ( ) = > {
// Cloud synastry history is best-effort; local history remains usable.
} ) ;
} )
. catch ( ( ) = > {
// Cloud synastry history is best-effort; local history remains usable.
} ) ;
} , [ accountId , profile ] ) ;
useEffect ( ( ) = > {
if ( ! accountId ) return ;
}
setChartLibrary ( ( current ) = > {
const next = upsertSelfChart ( current , profile ) ;
localStorage . setItem ( chartLibraryStorageKey ( accountId ) , JSON . stringify ( next ) ) ;
@@ -3745,6 +3752,188 @@ export default function Home() {
} ) ) ;
const modalOpen = activeAccountDialog !== null || onboardingPaywallOpen ;
chatActionsRef . current = {
onFeedback ( feedbackKey , requested ) {
setMessageFeedback ( ( current ) = > {
const next = { . . . current } ;
const value = toggleChatMessageFeedback ( current [ feedbackKey ] , requested ) ;
if ( value ) next [ feedbackKey ] = value ;
else delete next [ feedbackKey ] ;
return next ;
} ) ;
} ,
onCopy ( feedbackKey , text ) {
void copyAssistantMessage ( feedbackKey , text ) ;
} ,
onRegenerate ( renderKey ) {
regenerateLatestAnswer ( renderKey ) ;
} ,
onFollowUp ( question ) {
void send ( question , activeSession ? . theme ) ;
} ,
} ;
if ( activeAccountDialog !== null ) accountOverlayEpochRef . current += 1 ;
accountOverlayRef . current = {
title : activeAccountDialog ? accountDialogTitles [ activeAccountDialog ] : "" ,
dialogClass : activeAccountDialog ? accountDialogClasses [ activeAccountDialog ] : "" ,
signingOut ,
close : closeAccountDialog ,
takeOpenLibraryRequest() {
if ( ! openChartLibraryOnDialog . current ) return false ;
openChartLibraryOnDialog . current = false ;
return true ;
} ,
overlayRef : accountDialog ,
closeButtonRef : closeButton ,
renderProfile ( library ) {
return (
< >
{ accountError && < p className = "form-error" role = "alert" > { accountError } < / p > }
{ account . avatar && (
< section className = "sheet-section avatar-section" aria-labelledby = "avatar-section-title" >
< div className = "section-heading" >
< b id = "avatar-section-title" > 头 像 < / b >
< small > Beam 形 象 由 随 机 种 子 生 成 , 刷 新 和 换 设 备 后 保 持 一 致 < / small >
< / div >
< div className = "avatar-editor" >
< UserAvatar avatar = { account . avatar } size = { 96 } label = "当前头像预览" / >
< div className = "avatar-editor-controls" >
< div className = "avatar-palette-list" role = "radiogroup" aria-label = "头像配色" >
{ beamAvatarPalettes . map ( ( palette , index ) = > (
< button
key = { palette . name }
className = "avatar-palette"
type = "button"
role = "radio"
aria-checked = { account . avatar ? . palette === index }
aria-label = { palette . name }
title = { palette . name }
disabled = { avatarSaving }
onClick = { ( ) = > void persistAvatar ( { action : "set_palette" , palette : index } ) }
>
{ palette . colors . map ( ( color ) = > < span key = { color } style = { { backgroundColor : color } } / > ) }
< / button >
) ) }
< / div >
< button
className = "button-secondary avatar-randomize"
type = "button"
disabled = { avatarSaving }
onClick = { ( ) = > void persistAvatar ( { action : "randomize" } ) }
>
{ avatarSaving ? "保存中" : "换一个形象" }
< / button >
< / div >
< / div >
{ avatarNotice && < p className = "form-success avatar-notice" role = "status" > { avatarNotice } < / p > }
< / section >
) }
< section className = "sheet-section birth-section" >
< div className = "section-heading" > < b > 出 生 资 料 < / b > < small > 加 密 传 输 并 保 存 到 云 端 , 用 于 此 账 号 的 所 有 对 话 < / small > < / div >
< div className = "default-chart-card" aria-label = "当前默认星盘" >
< div >
< span > 当 前 默 认 星 盘 < / span >
< strong > { profileDraft . name . trim ( ) || "未命名" } < / strong >
< small > 角 色 : 本 人 < / small >
< / div >
< button className = "button-secondary" type = "button" onClick = { library . toggle } > 管 理 星 盘 库 < / button >
< / div >
{ library . open && (
< div className = "chart-library-panel" aria-label = "星盘库" >
< div className = "chart-library-group" >
< b > 本 人 < / b >
{ chartLibrary . filter ( ( record ) = > record . role === "self" ) . map ( ( record ) = > (
< article className = "chart-library-item" key = { record . id } >
< div >
< strong > { record . profile . name || "未命名" } < / strong >
< small > { record . profile . date } { record . profile . time } · { profilePlaceLabel ( record . profile ) } < / small >
< / div >
< span > 当 前 默 认 < / span >
< / article >
) ) }
< / div >
< div className = "chart-library-group" >
< b > 其 他 < / b >
{ chartLibrary . filter ( ( record ) = > record . role === "other" ) . length === 0 && < p className = "empty-library-copy" > 还 没 有 其 他 星 盘 。 < / p > }
{ chartLibrary . filter ( ( record ) = > record . role === "other" ) . map ( ( record ) = > (
< article className = "chart-library-item" key = { record . id } >
< div >
< strong > { record . profile . name || "未命名" } < / strong >
< small > { record . profile . date } { record . profile . time } · { profilePlaceLabel ( record . profile ) } < / small >
< / div >
< div className = "chart-library-actions" >
< select aria-label = "关系类型" value = { synastryRelationshipType } onChange = { ( event ) = > setSynastryRelationshipType ( event . target . value as SynastryRelationshipType ) } disabled = { synastryPendingId !== null } >
< option value = "romance" > 婚 恋 < / option >
< option value = "business" > 商 业 合 作 < / option >
< option value = "family" > 亲 友 / 家 庭 < / option >
< option value = "general" > 其 他 关 系 < / option >
< / select >
< button className = "button-secondary" type = "button" onClick = { ( ) = > void draftSynastryQuestionFromChart ( record , synastryRelationshipType ) } disabled = { synastryPendingId !== null } > { synastryPendingId === record . id ? "正在计算合盘..." : "用于合盘" } < / button >
< button className = "button-secondary" type = "button" onClick = { ( ) = > void makeDefaultChart ( record ) } disabled = { profileSaving } > 设 为 默 认 < / button >
< button className = "button-secondary danger-button" type = "button" onClick = { ( ) = > deleteOtherChart ( record . id ) } > 删 除 < / button >
< / div >
< / article >
) ) }
< / div >
{ synastryReportCard && (
< article className = "synastry-report-card" aria-label = "合盘结果摘要" >
< div >
< span > 合 盘 结 果 摘 要 < / span >
< strong > { synastryReportCard . partnerName } < / strong >
< small > Ashtakoot { synastryReportCard . score ? ? "?" } / { synastryReportCard . maxScore ? ? "?" } · { synastryReportCard . assessment || synastryReportCard . scoreBand || "待解释" } < / small >
< / div >
{ synastryReportCard . headline && < p > { synastryReportCard . headline } < / p > }
< details >
< summary > 查 看 证 据 < / summary >
< ul >
{ ( synastryReportCard . strengths || [ ] ) . map ( ( item ) = > < li key = { item } > { item } < / li > ) }
{ ( synastryReportCard . risks || [ ] ) . map ( ( item ) = > < li key = { item } > { item } < / li > ) }
< / ul >
< small > 下 一 步 证 据 : { ( synastryReportCard . nextEvidence || [ ] ) . join ( " / " ) || "双方 Dasha / UL-DK / D9 7宫" } < / small >
< / details >
< / article >
) }
{ synastryHistory . length > 0 && (
< div className = "synastry-history-list" aria-label = "合盘历史" >
< b > 合 盘 历 史 < / b >
{ synastryHistory . slice ( 0 , 5 ) . map ( ( item ) = > (
< button key = { item . id } type = "button" className = "synastry-history-item" onClick = { ( ) = > setSynastryReportCard ( item ) } >
< span > { item . partnerName } < / span >
< small > Ashtakoot { item . score ? ? "?" } / { item . maxScore ? ? "?" } · { item . assessment || item . scoreBand || "待解释" } < / small >
< / button >
) ) }
< / div >
) }
< form className = "profile-form chart-library-form" onSubmit = { saveOtherChart } >
< div className = "section-heading" > < b > 添 加 其 他 星 盘 < / b > < small > 用 于 合 盘 、 亲 友 盘 或 客 户 盘 。 < / small > < / div >
< ProfileFields value = { otherProfileDraft } onChange = { setOtherProfileDraft } nameInputId = "other-profile-name" / >
< button className = "button-primary save-profile" type = "submit" disabled = { ! account } > 添 加 到 星 盘 库 < / button >
< / form >
< / div >
) }
< / section >
{ profileNotice && < p className = "form-success" role = "status" > { profileNotice } < / p > }
< form className = "profile-form" onSubmit = { saveProfile } >
< ProfileFields value = { profileDraft } onChange = { setProfileDraft } nameInputId = "profile-name" / >
< button className = "button-primary save-profile" type = "submit" disabled = { profileSaving || ! account } > { profileSaving ? "保存中" : "保存出生资料" } < / button >
< / form >
< / >
) ;
} ,
renderLogout() {
return (
< >
< p className = "logout-copy" > 退 出 后 , 需 要 重 新 登 录 才 能 继 续 查 看 对 话 。 < / p >
{ accountError && < p className = "form-error" role = "alert" > { accountError } < / p > }
< div className = "dialog-actions" >
< button className = "button-secondary" type = "button" onClick = { closeAccountDialog } disabled = { signingOut } > 取 消 < / button >
< button className = "button-primary danger-primary" type = "button" onClick = { ( ) = > void signOut ( ) } disabled = { signingOut } > { signingOut ? "正在退出" : "确认退出" } < / button >
< / div >
< / >
) ;
} ,
} ;
return (
< SidebarProvider escapeBlocked = { accountMenuOpen || modalOpen } >
< main className = "chat-app" >
@@ -3788,11 +3977,11 @@ export default function Home() {
onOpenReports = { ( ) = > router . push ( "/reports" ) }
onSelectSession = { selectSession }
onSelectChart = { ( ) = > {
setChartLibraryOpen ( true ) ;
openChartLibraryOnDialog . current = true ;
openAccountDialog ( "profile" ) ;
} }
onAddChart = { ( ) = > {
setChartLibraryOpen ( true ) ;
openChartLibraryOnDialog . current = true ;
openAccountDialog ( "profile" ) ;
} }
onOpenProfile = { ( ) = > openAccountDialog ( "profile" ) }
@@ -4007,55 +4196,23 @@ export default function Home() {
< / div >
) : (
< div className = "message-list" aria-busy = { isLoading } >
{ chatMessageViews ( activeSession . messages , isLoading , activeStreamingText , activeStreamingActivity , activeStreamingThinking , activeStreamingSections , activeStreamingTimeline ) . map ( ( message , index , views ) = > {
const showActions = message . role === "assistant"
&& message . state === "settled"
&& Boolean ( message . text ) ;
const feedbackKey = ` ${ activeSession . id } : ${ message . renderKey } ` ;
const latestRegeneratableKey = ! isLoading && ! cancellationPending
? [ . . . views ] . reverse ( ) . find ( ( item ) = > (
item . role === "assistant" && item . state === "settled" && Boolean ( item . text )
) ) ? . renderKey
: undefined ;
const previousQuestion = views [ index - 1 ] ? . role === "user" ? views [ index - 1 ] ? . text : "" ;
const followUps = showActions
&& message . renderKey === latestRegeneratableKey
&& activeSession . sessionType === "consultation"
&& previousQuestion
? deriveConsultationFollowUps ( {
question : previousQuestion ,
answer : message.text ,
theme : activeSession.theme ,
entrypoint : isGeneralDailyFortuneQuestion ( previousQuestion ) ? "daily_starlanguage" : null ,
} )
: [ ] ;
return (
< div className = "message-entry" key = { message . renderKey } >
< ChatMessageRow message = { message } / >
{ showActions && (
< ChatMessageActions
feedback = { messageFeedback [ feedbackKey ] }
copied = { copiedMessageKey === feedbackKey }
canRegenerate = { message . renderKey === latestRegeneratableKey }
onFeedback = { ( requested ) = > setMessageFeedback ( ( current ) = > {
const next = { . . . current } ;
const value = toggleChatMessageFeedback ( current [ feedbackKey ] , requested ) ;
if ( value ) next [ feedbackKey ] = value ;
else delete next [ feedbackKey ] ;
return next ;
} ) }
onCopy = { ( ) = > void copyAssistantMessage ( feedbackKey , message . text ) }
onRegenerate = { ( ) = > regenerateLatestAnswer ( message . renderKey ) }
/ >
) }
< ConversationFollowUps
questions = { followUps }
disabled = { productEntrypointsDisabled }
onSelect = { ( question ) = > void send ( question , activeSession . theme ) }
/ >
< / div >
) ;
} ) }
< ChatTranscript
messages = { activeSession . messages }
loading = { isLoading }
streamingText = { activeStreamingText }
streamingActivity = { activeStreamingActivity }
streamingThinking = { activeStreamingThinking }
streamingSections = { activeStreamingSections }
streamingTimeline = { activeStreamingTimeline }
sessionId = { activeSession . id }
sessionType = { activeSession . sessionType }
theme = { activeSession . theme }
messageFeedback = { messageFeedback }
copiedMessageKey = { copiedMessageKey }
cancellationPending = { cancellationPending }
productEntrypointsDisabled = { productEntrypointsDisabled }
actionsRef = { chatActionsRef }
/ >
{ activeError && < p className = "error-message" role = "alert" > { activeError } < / p > }
< / div >
) }
@@ -4145,161 +4302,12 @@ export default function Home() {
< / div > }
< / SidebarInset >
{ activeAccountDialog !== null && (
< div className = "account-modal-overlay" onMouseDown = { closeAccountDialog } >
< section className = { ` account-modal ${ accountDialogClasses [ activeAccountDialog ] } ` } ref = { accountDialog } role = "dialog" aria-modal = "true" aria-labelledby = "account-dialog-title" onMouseDown = { ( event ) = > event . stopPropagation ( ) } >
< header className = "account-modal-header" >
< h2 id = "account-dialog-title" > { accountDialogTitles [ activeAccountDialog ] } < / h2 >
< button className = "dialog-close" ref = { closeButton } aria-label = "关闭" type = "button" onClick = { closeAccountDialog } disabled = { signingOut } > < X aria-hidden = "true" / > < / button >
< / header >
{ activeAccountDialog === "profile" && (
< >
{ accountError && < p className = "form-error" role = "alert" > { accountError } < / p > }
{ account . avatar && (
< section className = "sheet-section avatar-section" aria-labelledby = "avatar-section-title" >
< div className = "section-heading" >
< b id = "avatar-section-title" > 头 像 < / b >
< small > Beam 形 象 由 随 机 种 子 生 成 , 刷 新 和 换 设 备 后 保 持 一 致 < / small >
< / div >
< div className = "avatar-editor" >
< UserAvatar avatar = { account . avatar } size = { 96 } label = "当前头像预览" / >
< div className = "avatar-editor-controls" >
< div className = "avatar-palette-list" role = "radiogroup" aria-label = "头像配色" >
{ beamAvatarPalettes . map ( ( palette , index ) = > (
< button
key = { palette . name }
className = "avatar-palette"
type = "button"
role = "radio"
aria-checked = { account . avatar ? . palette === index }
aria-label = { palette . name }
title = { palette . name }
disabled = { avatarSaving }
onClick = { ( ) = > void persistAvatar ( { action : "set_palette" , palette : index } ) }
>
{ palette . colors . map ( ( color ) = > < span key = { color } style = { { backgroundColor : color } } / > ) }
< / button >
) ) }
< / div >
< button
className = "button-secondary avatar-randomize"
type = "button"
disabled = { avatarSaving }
onClick = { ( ) = > void persistAvatar ( { action : "randomize" } ) }
>
{ avatarSaving ? "保存中" : "换一个形象" }
< / button >
< / div >
< / div >
{ avatarNotice && < p className = "form-success avatar-notice" role = "status" > { avatarNotice } < / p > }
< / section >
) }
< section className = "sheet-section birth-section" >
< div className = "section-heading" > < b > 出 生 资 料 < / b > < small > 加 密 传 输 并 保 存 到 云 端 , 用 于 此 账 号 的 所 有 对 话 < / small > < / div >
< div className = "default-chart-card" aria-label = "当前默认星盘" >
< div >
< span > 当 前 默 认 星 盘 < / span >
< strong > { profileDraft . name . trim ( ) || "未命名" } < / strong >
< small > 角 色 : 本 人 < / small >
< / div >
< button className = "button-secondary" type = "button" onClick = { ( ) = > setChartLibraryOpen ( ( current ) = > ! current ) } > 管 理 星 盘 库 < / button >
< / div >
{ chartLibraryOpen && (
< div className = "chart-library-panel" aria-label = "星盘库" >
< div className = "chart-library-group" >
< b > 本 人 < / b >
{ chartLibrary . filter ( ( record ) = > record . role === "self" ) . map ( ( record ) = > (
< article className = "chart-library-item" key = { record . id } >
< div >
< strong > { record . profile . name || "未命名" } < / strong >
< small > { record . profile . date } { record . profile . time } · { profilePlaceLabel ( record . profile ) } < / small >
< / div >
< span > 当 前 默 认 < / span >
< / article >
) ) }
< / div >
< div className = "chart-library-group" >
< b > 其 他 < / b >
{ chartLibrary . filter ( ( record ) = > record . role === "other" ) . length === 0 && < p className = "empty-library-copy" > 还 没 有 其 他 星 盘 。 < / p > }
{ chartLibrary . filter ( ( record ) = > record . role === "other" ) . map ( ( record ) = > (
< article className = "chart-library-item" key = { record . id } >
< div >
< strong > { record . profile . name || "未命名" } < / strong >
< small > { record . profile . date } { record . profile . time } · { profilePlaceLabel ( record . profile ) } < / small >
< / div >
< div className = "chart-library-actions" >
< select aria-label = "关系类型" value = { synastryRelationshipType } onChange = { ( event ) = > setSynastryRelationshipType ( event . target . value as SynastryRelationshipType ) } disabled = { synastryPendingId !== null } >
< option value = "romance" > 婚 恋 < / option >
< option value = "business" > 商 业 合 作 < / option >
< option value = "family" > 亲 友 / 家 庭 < / option >
< option value = "general" > 其 他 关 系 < / option >
< / select >
< button className = "button-secondary" type = "button" onClick = { ( ) = > void draftSynastryQuestionFromChart ( record , synastryRelationshipType ) } disabled = { synastryPendingId !== null } > { synastryPendingId === record . id ? "正在计算合盘..." : "用于合盘" } < / button >
< button className = "button-secondary" type = "button" onClick = { ( ) = > void makeDefaultChart ( record ) } disabled = { profileSaving } > 设 为 默 认 < / button >
< button className = "button-secondary danger-button" type = "button" onClick = { ( ) = > deleteOtherChart ( record . id ) } > 删 除 < / button >
< / div >
< / article >
) ) }
< / div >
{ synastryReportCard && (
< article className = "synastry-report-card" aria-label = "合盘结果摘要" >
< div >
< span > 合 盘 结 果 摘 要 < / span >
< strong > { synastryReportCard . partnerName } < / strong >
< small > Ashtakoot { synastryReportCard . score ? ? "?" } / { synastryReportCard . maxScore ? ? "?" } · { synastryReportCard . assessment || synastryReportCard . scoreBand || "待解释" } < / small >
< / div >
{ synastryReportCard . headline && < p > { synastryReportCard . headline } < / p > }
< details >
< summary > 查 看 证 据 < / summary >
< ul >
{ ( synastryReportCard . strengths || [ ] ) . map ( ( item ) = > < li key = { item } > { item } < / li > ) }
{ ( synastryReportCard . risks || [ ] ) . map ( ( item ) = > < li key = { item } > { item } < / li > ) }
< / ul >
< small > 下 一 步 证 据 : { ( synastryReportCard . nextEvidence || [ ] ) . join ( " / " ) || "双方 Dasha / UL-DK / D9 7宫" } < / small >
< / details >
< / article >
) }
{ synastryHistory . length > 0 && (
< div className = "synastry-history-list" aria-label = "合盘历史" >
< b > 合 盘 历 史 < / b >
{ synastryHistory . slice ( 0 , 5 ) . map ( ( item ) = > (
< button key = { item . id } type = "button" className = "synastry-history-item" onClick = { ( ) = > setSynastryReportCard ( item ) } >
< span > { item . partnerName } < / span >
< small > Ashtakoot { item . score ? ? "?" } / { item . maxScore ? ? "?" } · { item . assessment || item . scoreBand || "待解释" } < / small >
< / button >
) ) }
< / div >
) }
< form className = "profile-form chart-library-form" onSubmit = { saveOtherChart } >
< div className = "section-heading" > < b > 添 加 其 他 星 盘 < / b > < small > 用 于 合 盘 、 亲 友 盘 或 客 户 盘 。 < / small > < / div >
< ProfileFields value = { otherProfileDraft } onChange = { setOtherProfileDraft } nameInputId = "other-profile-name" / >
< button className = "button-primary save-profile" type = "submit" disabled = { ! account } > 添 加 到 星 盘 库 < / button >
< / form >
< / div >
) }
< / section >
{ profileNotice && < p className = "form-success" role = "status" > { profileNotice } < / p > }
< form className = "profile-form" onSubmit = { saveProfile } >
< ProfileFields value = { profileDraft } onChange = { setProfileDraft } nameInputId = "profile-name" / >
< button className = "button-primary save-profile" type = "submit" disabled = { profileSaving || ! account } > { profileSaving ? "保存中" : "保存出生资料" } < / button >
< / form >
< / >
) }
{ activeAccountDialog === "logout" && (
< >
< p className = "logout-copy" > 退 出 后 , 需 要 重 新 登 录 才 能 继 续 查 看 对 话 。 < / p >
{ accountError && < p className = "form-error" role = "alert" > { accountError } < / p > }
< div className = "dialog-actions" >
< button className = "button-secondary" type = "button" onClick = { closeAccountDialog } disabled = { signingOut } > 取 消 < / button >
< button className = "button-primary danger-primary" type = "button" onClick = { ( ) = > void signOut ( ) } disabled = { signingOut } > { signingOut ? "正在退出" : "确认退出" } < / button >
< / div >
< / >
) }
< / section >
< / div >
) }
< AccountDialogOverlay
open = { activeAccountDialog !== null }
dialog = { activeAccountDialog }
openEpoch = { activeAccountDialog !== null ? accountOverlayEpochRef.current : 0 }
modelRef = { accountOverlayRef }
/ >
{ onboardingPaywallOpen && account && (
< OnboardingRedeemPaywall
credits = { account . credits }