fix(home): send starter cards instead of filling the composer
Clicking today's reading or a topic card should open a consultation and wait for the model, not leave the question in the input box. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+2
-2
@@ -172,12 +172,12 @@ The base unit is 4px. Tokens are `--space-1: 4px`, `--space-2: 8px`, `--space-3:
|
||||
- **Structure:** topic label, question, directional icon. Categories are not numbered because they have no required order. At tablet widths, the cards stack into one column so Chinese questions keep natural phrase boundaries beside the persistent sidebar.
|
||||
- **Surface:** warm light cards; the lead card uses the pale brown emphasis surface and border instead of a dark block.
|
||||
- **States:** default, hover, active, focus, disabled, loading, fallback notice.
|
||||
- **Visibility:** the initial cards, one per consultation domain, remain visible while the user types or chooses a question. They leave only after the question is submitted and the session receives its first user message.
|
||||
- **Visibility:** the initial cards, one per consultation domain, remain visible while the user types a custom question. Clicking a card starts that consultation immediately instead of filling the composer; the cards leave once the session receives its first user message.
|
||||
|
||||
### Product entrypoint card
|
||||
|
||||
- **Structure:** the homepage daily-reading and birth-time cards are single native-button targets stretched across their article surface. Content remains semantic card copy; a compact action label and arrow sit at the lower right.
|
||||
- **Copy:** the composer and chat history show only the public labels “深入看今日”, “生时校正”, or “再次校正”. Private model instructions are selected by a closed entrypoint identifier and expanded only on the server.
|
||||
- **Copy:** chat history shows only the public labels “深入看今日”, “生时校正”, or “再次校正”. Clicking the daily card starts a consultation session with that public label and waits for the model; it does not place the question in the composer. Private model instructions are selected by a closed entrypoint identifier and expanded only on the server.
|
||||
- **States:** default, whole-card hover, pressed, focus-visible, and disabled. The card surface—not an inner promotional button—carries the interaction feedback.
|
||||
- **Responsive:** cards stack below 768px without introducing a large nested button; the footer keeps supporting copy flexible and the action label on one line.
|
||||
- **Accessibility:** each card exposes exactly one native button with a descriptive accessible name, preserves a visible focus ring, and meets the full-card touch target.
|
||||
|
||||
@@ -2067,8 +2067,8 @@ export default function Home() {
|
||||
}
|
||||
}
|
||||
|
||||
async function startNewChat() {
|
||||
if (!account || !modelCatalog || creatingSession) return;
|
||||
async function startNewChat(): Promise<ChatSession | null> {
|
||||
if (!account || !modelCatalog || creatingSession) return null;
|
||||
const nextSession = createSession(modelCatalog.defaultModelId);
|
||||
const previousSessionId = activeSession?.id ?? "";
|
||||
setCreatingSession(true);
|
||||
@@ -2081,6 +2081,7 @@ export default function Home() {
|
||||
setRequestError(null);
|
||||
try {
|
||||
await persistSession(nextSession, "create");
|
||||
return nextSession;
|
||||
} catch (caught) {
|
||||
setSessions((current) => current.filter((session) => session.id !== nextSession.id));
|
||||
setActiveSessionId(previousSessionId);
|
||||
@@ -2088,6 +2089,7 @@ export default function Home() {
|
||||
sessionId: previousSessionId,
|
||||
message: caught instanceof Error ? caught.message : "新对话未能保存到云端。",
|
||||
});
|
||||
return null;
|
||||
} finally {
|
||||
setCreatingSession(false);
|
||||
}
|
||||
@@ -2498,8 +2500,37 @@ export default function Home() {
|
||||
window.requestAnimationFrame(() => composerInput.current?.focus());
|
||||
}
|
||||
|
||||
function draftDailyStarlanguageQuestion() {
|
||||
chooseSuggestedQuestion(
|
||||
async function startSuggestedConsultation(
|
||||
question: string,
|
||||
theme?: Theme,
|
||||
entrypoint: ConsultationEntrypoint | null = null,
|
||||
) {
|
||||
if (
|
||||
pendingSessionId
|
||||
|| cancellationInFlight.current
|
||||
|| pendingConsultation.current
|
||||
|| creatingSession
|
||||
) return;
|
||||
const trimmed = question.trim();
|
||||
if (!trimmed || !account || !modelCatalog) return;
|
||||
|
||||
let targetSession = activeSession
|
||||
&& activeSession.sessionType === "consultation"
|
||||
&& activeSession.messages.length === 0
|
||||
? activeSession
|
||||
: null;
|
||||
if (!targetSession) {
|
||||
targetSession = await startNewChat();
|
||||
if (!targetSession) return;
|
||||
}
|
||||
|
||||
await send(trimmed, theme, entrypoint, null, targetSession.id, {
|
||||
sessionOverride: targetSession,
|
||||
});
|
||||
}
|
||||
|
||||
function startDailyStarlanguageConsultation() {
|
||||
void startSuggestedConsultation(
|
||||
dailyStarlanguageQuestion,
|
||||
"timing",
|
||||
"daily_starlanguage",
|
||||
@@ -3749,7 +3780,7 @@ export default function Home() {
|
||||
type="button"
|
||||
aria-label={natalMinuteAvailable ? dailyStarlanguageQuestion : "查看今日运势"}
|
||||
disabled={productEntrypointsDisabled}
|
||||
onClick={draftDailyStarlanguageQuestion}
|
||||
onClick={startDailyStarlanguageConsultation}
|
||||
/>
|
||||
<div className="product-entrypoint-copy">
|
||||
<h2 id="daily-starlanguage-title">{natalMinuteAvailable ? "今日星语" : "每日运势"}</h2>
|
||||
@@ -3806,8 +3837,8 @@ export default function Home() {
|
||||
key={`${item.theme}-${item.text}`}
|
||||
type="button"
|
||||
aria-label={`${theme?.label || "开始"}:${item.text}`}
|
||||
disabled={!hydrated || Boolean(pendingSessionId) || cancellationPending || !account || !modelCatalog}
|
||||
onClick={() => chooseSuggestedQuestion(item.text, item.theme)}
|
||||
disabled={productEntrypointsDisabled}
|
||||
onClick={() => void startSuggestedConsultation(item.text, item.theme)}
|
||||
>
|
||||
<span className="starter-content">
|
||||
<b>{theme?.label || "开始"}</b>
|
||||
|
||||
@@ -81,7 +81,7 @@ test("every external draft writer keeps working through the page-owned setters",
|
||||
assert.match(pageSource, /onChange=\{\(event\) => \{\n\s*setDraft\(event\.target\.value\);\n\s*setDraftTheme\(null\);\n\s*setDraftEntrypoint\(null\);\n\s*setComposerNotice\(""\);\n\s*\}\}/);
|
||||
|
||||
// When: each existing write path is inspected.
|
||||
const chooseSuggested = sourceBetween(pageSource, "function chooseSuggestedQuestion(", "function draftDailyStarlanguageQuestion");
|
||||
const chooseSuggested = sourceBetween(pageSource, "function chooseSuggestedQuestion(", "async function startSuggestedConsultation");
|
||||
const startNewChat = sourceBetween(pageSource, "async function startNewChat()", "function selectSession(");
|
||||
const selectSession = sourceBetween(pageSource, "function selectSession(sessionId: string)", "async function selectSessionModel");
|
||||
const saveOnboardingName = sourceBetween(pageSource, "async function saveOnboardingName()", "async function saveOnboardingBirth");
|
||||
@@ -93,8 +93,11 @@ test("every external draft writer keeps working through the page-owned setters",
|
||||
"setRequestError({",
|
||||
);
|
||||
|
||||
// Then: suggestions fill, session switches clear, stop restores and send clears.
|
||||
// Then: synastry still fills the composer; session switches clear, stop restores and send clears.
|
||||
assert.match(chooseSuggested, /setDraft\(question\);\n\s*setDraftTheme\(theme \?\? null\);\n\s*setDraftEntrypoint\(entrypoint\);/);
|
||||
const startSuggested = sourceBetween(pageSource, "async function startSuggestedConsultation(", "function startDailyStarlanguageConsultation");
|
||||
assert.match(startSuggested, /await send\(trimmed, theme, entrypoint, null, targetSession\.id/);
|
||||
assert.doesNotMatch(startSuggested, /setDraft\(/);
|
||||
assert.match(startNewChat, /setDraft\(""\)/);
|
||||
assert.match(selectSession, /setDraft\(""\)/);
|
||||
assert.match(saveOnboardingName, /composerDraftSnapshot\(\)\.replace\(/);
|
||||
|
||||
@@ -129,7 +129,7 @@ test("ordinary product drafts keep the public question and clear hidden routing
|
||||
assert.match(source, /dailyStarlanguageQuestion/);
|
||||
assert.match(source, /从今日问起/);
|
||||
assert.match(source, /深入看今日/);
|
||||
assert.match(source, /chooseSuggestedQuestion\(\s*dailyStarlanguageQuestion,\s*"timing",\s*"daily_starlanguage"/);
|
||||
assert.match(source, /startSuggestedConsultation\(\s*dailyStarlanguageQuestion,\s*"timing",\s*"daily_starlanguage"/);
|
||||
assert.match(source, /consultEntrypoint === "birth_time_rectification"/);
|
||||
assert.match(source, /isRectificationHandoffQuestion\(question\)/);
|
||||
assert.match(source, /openRectificationFromHomepage\(pendingQuestion\)/);
|
||||
|
||||
@@ -46,7 +46,7 @@ test("default starter questions derive every canonical domain with evidence and
|
||||
assert.match(pageSource, /defaultGuidedJyotishTopics/);
|
||||
assert.match(pageSource, /starterSuggestions\.map/);
|
||||
assert.match(pageSource, /starterThemes\.find\(\(candidate\) => candidate\.id === item\.theme\)/);
|
||||
assert.match(pageSource, /chooseSuggestedQuestion\(item\.text, item\.theme\)/);
|
||||
assert.match(pageSource, /void startSuggestedConsultation\(item\.text, item\.theme\)/);
|
||||
assert.equal(consultationDomainIds.length, 10);
|
||||
assert.deepEqual(consultationDomainRegistry.map((domain) => domain.id), [...consultationDomainIds]);
|
||||
assert.match(guidedTopicsSource, /consultationDomainRegistry\.map/);
|
||||
@@ -106,10 +106,27 @@ test("profiles without a usable birth minute receive user-centered starter promp
|
||||
assert.match(themeSectionHeading, /natalMinuteAvailable/);
|
||||
assert.match(themeSectionHeading, /出生时间不足以支持的部分,我会明确说明,不会补造具体分钟/);
|
||||
assert.match(pageSource, /GENERAL_NO_MINUTE_DAILY_FORTUNE_QUESTION/);
|
||||
assert.match(pageSource, /chooseSuggestedQuestion\(\s*dailyStarlanguageQuestion,\s*"timing",\s*"daily_starlanguage"/);
|
||||
assert.match(pageSource, /startSuggestedConsultation\(\s*dailyStarlanguageQuestion,\s*"timing",\s*"daily_starlanguage"/);
|
||||
assert.doesNotMatch(pageSource, /personalChartAvailable \? "daily_starlanguage" : null/);
|
||||
});
|
||||
|
||||
test("homepage daily and topic cards start a consultation instead of filling the composer", () => {
|
||||
const starterSend = sourceBetween(
|
||||
pageSource,
|
||||
"async function startSuggestedConsultation(",
|
||||
"function startDailyStarlanguageConsultation",
|
||||
);
|
||||
|
||||
assert.match(pageSource, /onClick=\{startDailyStarlanguageConsultation\}/);
|
||||
assert.match(pageSource, /onClick=\{\(\) => void startSuggestedConsultation\(item\.text, item\.theme\)\}/);
|
||||
assert.doesNotMatch(pageSource, /draftDailyStarlanguageQuestion/);
|
||||
assert.doesNotMatch(pageSource, /onClick=\{\(\) => chooseSuggestedQuestion\(item\.text, item\.theme\)\}/);
|
||||
assert.match(starterSend, /await startNewChat\(\)/);
|
||||
assert.match(starterSend, /await send\(trimmed, theme, entrypoint, null, targetSession\.id/);
|
||||
assert.doesNotMatch(starterSend, /setDraft\(/);
|
||||
assert.doesNotMatch(starterSend, /composerInput\.current\?\.focus/);
|
||||
});
|
||||
|
||||
|
||||
test("keeps session history clickable while another session is answering", () => {
|
||||
// Given: the page-owned selection callback and the app sidebar session action.
|
||||
|
||||
Reference in New Issue
Block a user