diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ac5cbc6a..4527f605 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@base-ui/react": "^1.6.0", + "@gsap/react": "^2.1.2", "@mastra/core": "^1.50.1", "@supabase/ssr": "^0.12.3", "@supabase/supabase-js": "^2.110.5", @@ -18,6 +19,7 @@ "clsx": "^2.1.1", "date-fns": "^4.4.0", "drizzle-orm": "^0.45.2", + "gsap": "^3.15.0", "lucide-react": "^1.24.0", "next": "16.2.10", "pg": "^8.22.0", @@ -1286,6 +1288,16 @@ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, + "node_modules/@gsap/react": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@gsap/react/-/react-2.1.2.tgz", + "integrity": "sha512-JqliybO1837UcgH2hVOM4VO+38APk3ECNrsuSM4MuXp+rbf+/2IG2K1YJiqfTcXQHH7XlA0m3ykniFYstfq0Iw==", + "license": "SEE LICENSE AT https://gsap.com/standard-license", + "peerDependencies": { + "gsap": "^3.12.5", + "react": ">=17" + } + }, "node_modules/@hono/node-server": { "version": "1.19.14", "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", @@ -6565,6 +6577,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/gsap": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz", + "integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==", + "license": "Standard 'no charge' license: https://gsap.com/standard-license." + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 51eb115a..bff01bc2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@base-ui/react": "^1.6.0", + "@gsap/react": "^2.1.2", "@mastra/core": "^1.50.1", "@supabase/ssr": "^0.12.3", "@supabase/supabase-js": "^2.110.5", @@ -26,6 +27,7 @@ "clsx": "^2.1.1", "date-fns": "^4.4.0", "drizzle-orm": "^0.45.2", + "gsap": "^3.15.0", "lucide-react": "^1.24.0", "next": "16.2.10", "pg": "^8.22.0", diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 5cb021e2..7ff1c44b 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -785,3 +785,574 @@ input:disabled, select:disabled { color: var(--color-ink-tertiary); background: .conversational-composer-footer { align-items: stretch; flex-direction: column; } .conversational-composer-footer button, .conversational-confirmation button, .conversational-original-question button { width: 100%; } } +/* Starter workbench */ +.welcome { + width: min(1040px, 100%); + padding: var(--space-8) 0 var(--space-12); +} + +.starter-list { + width: min(1040px, 100%); + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-6); + border: 0; +} + +.starter-hero, +.product-entrypoints, +.starter-themes { + width: 100%; + min-width: 0; +} + +.starter-hero { + position: relative; + min-height: 220px; + overflow: hidden; + display: grid; + grid-template-columns: minmax(0, 1fr) 250px; + align-items: center; + gap: var(--space-8); + padding: var(--space-10); + border: 1px solid color-mix(in srgb, var(--color-action) 14%, var(--color-border)); + border-radius: var(--radius-xl); + background: radial-gradient(circle at 82% 36%, rgba(133, 67, 47, .12), transparent 33%), linear-gradient(135deg, #fbfaf7 0%, #f5f1eb 100%); +} + +.starter-hero::after { + position: absolute; + inset: 0; + opacity: .2; + pointer-events: none; + background-image: radial-gradient(rgba(29, 29, 31, .24) .45px, transparent .45px); + background-size: 6px 6px; + content: ""; + mask-image: linear-gradient(to right, transparent 8%, #000 72%); +} + +.starter-hero-copy { + position: relative; + z-index: 1; + max-width: 720px; +} + +.starter-greeting { + margin: 0 0 var(--space-3); + color: var(--color-action); + font-size: var(--type-body-sm); + font-weight: 600; + letter-spacing: .02em; +} + +.starter-hero h1 { + max-width: 980px; + margin: 0; + color: var(--color-ink); + font-family: var(--font-display); + font-size: clamp(42px, 5.4vw, 68px); + font-weight: 400; + letter-spacing: -.055em; + line-height: .98; + text-wrap: balance; +} + +.starter-hero-note { + max-width: 590px; + margin: var(--space-5) 0 0; + color: var(--color-ink-secondary); + font-size: var(--type-body-sm); + line-height: 1.7; + text-wrap: pretty; +} + +.starter-celestial-visual { + position: relative; + z-index: 1; + width: 232px; + height: 178px; + justify-self: end; +} + +.starter-orbit { + position: absolute; + border: 1px solid color-mix(in srgb, var(--color-action) 34%, transparent); + border-radius: 50%; +} + +.starter-orbit-outer { + inset: 8px 10px; + transform: rotate(-12deg); +} + +.starter-orbit-inner { + inset: 38px 50px; + border-color: color-mix(in srgb, var(--color-ink-tertiary) 42%, transparent); + transform: rotate(20deg); +} + +.starter-orbit-dot { + position: absolute; + width: 9px; + height: 9px; + border-radius: 50%; + background: var(--color-action); + box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-action-soft) 72%, transparent); +} + +.starter-orbit-dot-primary { + top: 26px; + right: 37px; +} + +.starter-orbit-dot-secondary { + bottom: 31px; + left: 31px; + width: 6px; + height: 6px; + background: var(--color-ink-tertiary); + box-shadow: none; +} + +.starter-celestial-mark { + position: absolute; + top: 50%; + left: 50%; + width: 54px; + height: 54px; + border: 1px solid color-mix(in srgb, var(--color-action) 18%, var(--color-border)); + border-radius: 50%; + background: color-mix(in srgb, var(--color-canvas) 88%, transparent) url("/jyotish-logo.png") center / 32px 32px no-repeat; + box-shadow: 0 14px 36px -24px rgba(29, 29, 31, .48); + transform: translate(-50%, -50%); +} + +.product-entrypoints { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0; + overflow: hidden; + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + background: var(--color-canvas); +} + +.product-entrypoints small { + color: var(--color-ink-secondary); + line-height: 1.5; + font-size: var(--type-caption); +} + +.daily-starlanguage-card, +.birth-rectification-card { + min-height: 228px; + display: grid; + grid-template-rows: minmax(0, 1fr) auto; + gap: var(--space-6); + padding: var(--space-8); + border: 0; + border-radius: 0; + background: transparent; +} + +.birth-rectification-card { + border-left: 1px solid var(--color-border); +} + +.product-entrypoint-card { + position: relative; + overflow: hidden; + transition: background-color 180ms ease-out; +} + +.product-entrypoint-card::after { + position: absolute; + inset: 0; + opacity: 0; + pointer-events: none; + background: linear-gradient(112deg, transparent 22%, rgba(255, 255, 255, .54) 46%, transparent 68%); + content: ""; + transform: translateX(-48%); + transition: opacity 220ms ease-out, transform 520ms var(--ease-out); +} + +.product-entrypoint-hitarea { + position: absolute; + z-index: 3; + inset: 0; + width: 100%; + min-height: 0; + padding: 0; + border: 0; + border-radius: inherit; + background: transparent; + cursor: pointer; +} + +.product-entrypoint-card > :not(.product-entrypoint-hitarea) { + position: relative; + z-index: 1; + pointer-events: none; +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) { + border-color: transparent; + background: color-mix(in srgb, var(--color-action-soft) 58%, var(--color-canvas)); + transform: none; +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover)::after { + opacity: .65; + transform: translateX(48%); +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:not(:disabled):hover) .product-entrypoint-action { + transform: translateX(3px); +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:focus-visible) { + outline: 3px solid color-mix(in srgb, var(--color-focus) 56%, transparent); + outline-offset: -3px; +} + +.product-entrypoint-card:has(.product-entrypoint-hitarea:disabled) { + opacity: .65; +} + +.product-entrypoint-hitarea:disabled { + cursor: not-allowed; +} + +.product-entrypoint-copy { + max-width: 520px; +} + +.product-entrypoint-kicker { + display: block; + margin-bottom: var(--space-4); + color: var(--color-action); + font-size: var(--type-overline); + font-weight: 600; + letter-spacing: .12em; +} + +.product-entrypoint-copy h2 { + margin: 0; + color: var(--color-ink); + font-family: var(--font-display); + font-size: clamp(26px, 2.6vw, 34px); + font-weight: 400; + letter-spacing: -.035em; + line-height: 1.05; +} + +.product-entrypoint-copy p { + max-width: 460px; + margin: var(--space-3) 0 0; + color: var(--color-ink-secondary); + font-size: var(--type-body-sm); + line-height: 1.65; + text-wrap: pretty; +} + +.product-entrypoint-footer { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: var(--space-5); + padding-top: var(--space-5); + border-top: 1px solid color-mix(in srgb, var(--color-border) 76%, transparent); +} + +.product-entrypoint-footer small { + min-width: 0; + flex: 1; +} + +.product-entrypoint-action { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + justify-content: flex-end; + gap: var(--space-1); + color: var(--color-action); + font-size: var(--type-caption); + font-weight: 600; + white-space: nowrap; + transition: transform 180ms var(--ease-out); +} + +.product-entrypoint-action .starter-arrow { + width: 15px; + height: 15px; + color: currentColor; +} + +.starter-themes { + display: grid; + gap: var(--space-4); +} + +.starter-section-heading { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--space-5); +} + +.starter-section-heading h2 { + margin: 0; + color: var(--color-ink); + font-family: var(--font-display); + font-size: var(--type-title-lg); + font-weight: 400; + letter-spacing: -.025em; +} + +.starter-section-heading p { + margin: 0; + color: var(--color-ink-tertiary); + font-size: var(--type-caption); +} + +.starter-theme-accordion { + display: flex; + min-width: 0; + overflow: hidden; + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + background: var(--color-canvas-soft); +} + +.starter-theme-card { + position: relative; + min-width: 0; + min-height: 170px; + flex: 1 1 0; + display: grid; + grid-template-columns: minmax(0, 1fr) 20px; + align-items: end; + gap: var(--space-3); + padding: var(--space-5); + border: 0; + border-right: 1px solid var(--color-border); + background: transparent; + cursor: pointer; + text-align: left; + transition: background-color 220ms ease-out, flex-grow 360ms var(--ease-out); +} + +.starter-theme-card:last-child { + border-right: 0; +} + +.starter-theme-card:not(:disabled):hover, +.starter-theme-card:focus-visible { + flex-grow: 1.62; + background: color-mix(in srgb, var(--color-action-soft) 56%, var(--color-canvas)); +} + +.starter-theme-accordion:has(.starter-theme-card:not(:disabled):hover) .starter-theme-card:not(:hover), +.starter-theme-accordion:has(.starter-theme-card:focus-visible) .starter-theme-card:not(:focus-visible) { + flex-grow: .78; +} + +.starter-theme-card:not(:disabled):hover .starter-arrow, +.starter-theme-card:focus-visible .starter-arrow { + color: var(--color-action); + transform: translate(2px, -2px); +} + +.starter-content { + min-width: 0; + display: grid; + gap: var(--space-3); +} + +.starter-content b { + color: var(--color-action); + font-size: var(--type-body-sm); + font-weight: 600; + letter-spacing: .04em; +} + +.starter-content span { + max-width: 245px; + overflow: visible; + color: var(--color-ink); + font-family: var(--font-display); + font-size: var(--type-title-md); + line-height: 1.38; + text-overflow: clip; + text-wrap: pretty; + white-space: normal; +} + +.starter-arrow { + width: 17px; + height: 17px; + color: var(--color-ink-secondary); + transition: color 180ms ease-out, transform 180ms var(--ease-out); +} + +.starter-loading { + width: min(1040px, 100%); + margin-left: 0; + padding: var(--space-6); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + background: var(--color-canvas-soft); + color: var(--color-ink-secondary); + font-size: var(--type-body-sm); +} + +.starter-note { + margin: calc(var(--space-2) * -1) 0 0; + color: var(--color-ink-secondary); + line-height: 1.5; + font-size: var(--type-caption); +} + +.composer-wrap-starter .composer, +.composer-wrap-starter .composer-footer, +.composer-wrap-starter .composer-suggestions { + width: min(1040px, 100%); +} + +@media (min-width: 768px) and (max-width: 900px) { + .starter-hero { + grid-template-columns: minmax(0, 1fr) 180px; + padding: var(--space-8); + } + + .starter-celestial-visual { + width: 180px; + height: 150px; + } + + .product-entrypoints { + grid-template-columns: 1fr; + } + + .birth-rectification-card { + border-top: 1px solid var(--color-border); + border-left: 0; + } + + .starter-theme-accordion { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .starter-theme-card { + min-height: 136px; + border-bottom: 1px solid var(--color-border); + } + + .starter-theme-card:nth-child(2n) { + border-right: 0; + } + + .starter-theme-card:nth-last-child(-n + 2) { + border-bottom: 0; + } +} + +@media (max-width: 767px) { + .welcome { + padding: var(--space-4) 0 var(--space-8); + } + + .starter-list { + grid-template-columns: 1fr; + grid-template-rows: auto; + gap: var(--space-5); + } + + .starter-hero { + min-height: 0; + grid-template-columns: 1fr; + padding: var(--space-8) var(--space-6); + } + + .starter-hero h1 { + font-size: clamp(38px, 11vw, 54px); + } + + .starter-celestial-visual { + position: absolute; + top: var(--space-4); + right: calc(var(--space-8) * -1); + width: 180px; + height: 150px; + opacity: .34; + } + + .product-entrypoints { + grid-template-columns: 1fr; + } + + .daily-starlanguage-card, + .birth-rectification-card { + min-height: 210px; + padding: var(--space-6); + } + + .birth-rectification-card { + border-top: 1px solid var(--color-border); + border-left: 0; + } + + .starter-section-heading { + display: grid; + gap: var(--space-1); + } + + .starter-theme-accordion { + display: grid; + } + + .starter-theme-card { + min-height: 116px; + border-right: 0; + border-bottom: 1px solid var(--color-border); + } + + .starter-theme-card:last-child { + border-bottom: 0; + } + + .starter-theme-accordion:has(.starter-theme-card:not(:disabled):hover) .starter-theme-card:not(:hover), + .starter-theme-accordion:has(.starter-theme-card:focus-visible) .starter-theme-card:not(:focus-visible), + .starter-theme-card:not(:disabled):hover, + .starter-theme-card:focus-visible { + flex-grow: 1; + } + + .starter-content span { + max-width: 440px; + } +} + +@media (max-width: 480px) { + .starter-hero { + padding: var(--space-6) var(--space-5); + } + + .starter-hero-note { + margin-top: var(--space-4); + } + + .product-entrypoint-footer { + display: grid; + gap: var(--space-4); + } + + .product-entrypoint-action { + justify-self: start; + } + + .starter-content span { + font-size: var(--type-title-sm); + } +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 21d5607c..e4c07f5b 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -3,6 +3,8 @@ import Link from "next/link"; import dynamic from "next/dynamic"; import { ArrowUp, ArrowUpRight, Sparkles, Square, X } from "lucide-react"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; import { useEffect, useRef, useState } from "react"; import type { FormEvent, KeyboardEvent } from "react"; import { AppSidebar } from "@/components/app-sidebar"; @@ -71,7 +73,6 @@ import { consultationReportMarkdown } from "@/lib/consultation-report-export"; import { OnboardingAuthenticationError, type OnboardingContent, - createOnboardingFallbackGreeting, createStartGreeting, isCurrentOnboardingRequest, onboardingProfileFingerprint, @@ -90,6 +91,8 @@ import { } from "@/lib/public-models"; import { createBrowserSupabaseClient } from "@/lib/supabase/client"; +gsap.registerPlugin(useGSAP); + const BirthTimeRectification = dynamic( () => import("@/components/birth-time-rectification").then((module) => module.BirthTimeRectification), { @@ -235,6 +238,12 @@ const dailyStarlanguageCards: DailyStarlanguageCard[] = [ { trend: "适合观察资源流向:时间、注意力、金钱都算。", action: "检查一个正在消耗你的习惯,并给它设上限。", caution: "不要为了短期安心做长期成本高的选择。" }, ]; +function greetingForHour(hour: number): string { + if (hour < 11) return "早上好"; + if (hour < 18) return "下午好"; + return "晚上好"; +} + const emptyProfile: Profile = { name: "", date: "", @@ -806,6 +815,7 @@ export default function Home() { const closeButton = useRef(null); const redeemInput = useRef(null); const composerInput = useRef(null); + const starterWorkbench = useRef(null); const pendingConsultation = useRef(null); const cancellationRequests = useRef(new Map>()); const cancellationFeedbackRequest = useRef(null); @@ -974,7 +984,6 @@ export default function Home() { }, [accountId, profile]); const profileComplete = isProfileComplete(profile); - const birthTimeDisplay = birthTimeDisplayState(profile); const dailyStarlanguage = dailyStarlanguageCard ?? (profileComplete ? buildDailyStarlanguageCard(profile) : null); const onboardingPending = profileComplete && !onboarding && !onboardingError; const currentOnboardingMessage = onboardingJustCompleted @@ -989,6 +998,14 @@ export default function Home() { const shouldStreamOnboarding = !profileComplete; const presetMessageFinished = !shouldStreamOnboarding || presetMessageLength >= currentOnboardingMessage.length; const onboardingCardReady = presetMessageFinished || birthTimeAssessmentPhase !== null; + const starterSuggestions = themes.map((theme) => onboarding?.suggestions.find((item) => item.theme === theme.id) + ?? { theme: theme.id, text: theme.prompt }); + const starterHomeVisible = profileComplete + && presetMessageFinished + && !onboardingPending + && !rectificationSurfaceOpen + && !activeSession?.messages.length; + const daypartGreeting = greetingForHour(new Date().getHours()); useEffect(() => { const controller = new AbortController(); @@ -2684,6 +2701,39 @@ export default function Home() { continueRectificationQuestion.current(question); }, [rectificationInitialTurn, rectificationLoading, rectificationMutationPending]); + useGSAP(() => { + if (!starterHomeVisible || !starterWorkbench.current) return; + const motion = gsap.matchMedia(); + motion.add("(prefers-reduced-motion: no-preference)", () => { + const timeline = gsap.timeline({ defaults: { ease: "power3.out" } }); + timeline + .from(".starter-hero-copy > *", { + autoAlpha: 0, + duration: 0.62, + stagger: 0.07, + y: 18, + }) + .from(".starter-celestial-visual", { + autoAlpha: 0, + duration: 0.76, + scale: 0.94, + }, 0.08) + .from(".product-entrypoint-card", { + autoAlpha: 0, + duration: 0.54, + stagger: 0.08, + y: 20, + }, 0.2) + .from(".starter-theme-card", { + autoAlpha: 0, + duration: 0.46, + stagger: 0.055, + y: 14, + }, 0.34); + }); + return () => motion.revert(); + }, { dependencies: [activeSession?.id, starterHomeVisible], scope: starterWorkbench }); + function submit(event: FormEvent) { event.preventDefault(); if (!profileComplete) { @@ -2839,12 +2889,7 @@ export default function Home() { {profileComplete && onboardingJustCompleted && selectedBirthPlace(profileDraft) && } {profileComplete && onboardingJustCompleted && } - ) : ( - - )} + ) : null} {!profileComplete && onboardingStep === "birth" && onboardingCardReady && (
@@ -2903,9 +2948,24 @@ export default function Home() { {profileComplete && presetMessageFinished && !rectificationSurfaceOpen && (onboardingPending ? (
正在准备三个入门问题…
) : ( -
-
-
+
+
+
+

{daypartGreeting},{profile.name.trim()}。

+

今天想先理清什么?

+

从此刻最在意的事开始,我会结合你的星盘证据,帮你把问题拆得更清楚。

+
+ +
+ +
+
-
+
-
- {(onboarding?.suggestions ?? themes.map((item) => ({ theme: item.id, text: item.prompt }))).map((item) => { - const theme = themes.find((candidate) => candidate.id === item.theme); - return ( - - ); - })} - {onboardingError &&

Agent 的个性化入门问题暂时不可用,已显示安全的默认问题。

} + + +
+
+

或者,从一个主题开始

+

选择最接近你当下的问题。

+
+
+ {starterSuggestions.map((item) => { + const theme = themes.find((candidate) => candidate.id === item.theme); + return ( + + ); + })} +
+
+ {onboardingError &&

个性化问题暂时不可用,已显示安全的默认问题。

}
))}
@@ -3000,7 +3063,7 @@ export default function Home() { )}
- {!rectificationSurfaceOpen &&
+ {!rectificationSurfaceOpen &&
{activeSuggestions.length > 0 && (
{activeSuggestions.map((question) => ( diff --git a/frontend/tests/consultation-entrypoint.test.ts b/frontend/tests/consultation-entrypoint.test.ts index a25f8404..cb7f6b37 100644 --- a/frontend/tests/consultation-entrypoint.test.ts +++ b/frontend/tests/consultation-entrypoint.test.ts @@ -245,3 +245,21 @@ test("homepage entrypoints use two whole-card native actions", () => { assert.equal(wholeCardActions.length, 2); assert.doesNotMatch(source, /className="daily-starlanguage-heading">[\s\S]{0,180}