feat: polish Jyotisha mobile chat UI

This commit is contained in:
Jesse_Chen
2026-07-16 11:02:33 +08:00
parent da93fd7fd0
commit 1b91667087
9 changed files with 82 additions and 57 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# Ayanam Web
# Jyotisha Web
Ayanam`yinduzhanxing` 的聊天式消费者 Web 前端。它使用 Next.js App Router 提供用户与 Mastra Agent 的连续问答,同时继续复用仓库现有的 Python 印度占星计算工作流。它不生成固定报告。
Jyotisha`yinduzhanxing` 的聊天式消费者 Web 前端。它使用 Next.js App Router 提供用户与 Mastra Agent 的连续问答,同时继续复用仓库现有的 Python 印度占星计算工作流。它不生成固定报告。
## 架构
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+1 -1
View File
@@ -9,7 +9,7 @@ const outputPath = resolve(
);
const response = await fetch(SOURCE_URL, {
headers: { "user-agent": "Ayanam location-data refresh" },
headers: { "user-agent": "Jyotisha location-data refresh" },
});
if (!response.ok) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 66 KiB

+32 -26
View File
@@ -26,7 +26,8 @@ button:disabled { cursor: default; opacity: .46; }
.chat-app { width: 100%; height: 100dvh; min-height: 0; overflow: hidden; display: grid; grid-template-columns: 272px minmax(0, 1fr); background: var(--surface); }
.sidebar { height: 100%; min-height: 0; overflow: hidden; display: flex; flex-direction: column; padding: 16px 12px 12px; border-right: 1px solid rgba(120, 118, 111, .26); background: rgba(235, 233, 227, .86); backdrop-filter: saturate(130%) blur(20px); }
.brand-row { height: 44px; display: flex; align-items: center; gap: 10px; padding: 0 8px; font-size: 17px; letter-spacing: -.02em; }
.brand-mark, .welcome-mark, .auth-brand span { width: 28px; height: 28px; display: grid; place-items: center; border: 1px solid #c6a99f; color: var(--accent); font-size: 15px; font-weight: 600; line-height: 1; }
.brand-mark, .welcome-mark, .auth-brand span { width: 28px; height: 28px; display: block; flex: 0 0 auto; background: url("/jyotish-logo.png") center / contain no-repeat; font-size: 0; }
.sidebar-close, .mobile-menu, .sidebar-backdrop { display: none; }
.new-chat { width: 100%; min-height: 44px; display: flex; align-items: center; justify-content: center; gap: 7px; margin: 14px 0 20px; padding: 0 14px; border: 1px solid var(--ink); border-radius: 10px; background: var(--ink); color: white; cursor: pointer; font-size: 13px; font-weight: 650; transition: background-color 140ms ease-out, transform 100ms ease-out; }
.new-chat span { font-size: 18px; font-weight: 400; line-height: 1; }
.new-chat:not(:disabled):active, .button-primary:not(:disabled):active, .button-secondary:not(:disabled):active, .credit-button:not(:disabled):active, .generated-list button:active { transform: scale(.97); }
@@ -42,20 +43,21 @@ button:disabled { cursor: default; opacity: .46; }
.sidebar-footer { margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(120, 118, 111, .22); }
.profile-trigger { width: 100%; min-height: 52px; display: grid; grid-template-columns: 34px minmax(0, 1fr) 18px; align-items: center; gap: 9px; padding: 5px 7px; border: 0; border-radius: 9px; background: transparent; cursor: pointer; text-align: left; transition: background-color 120ms ease-out, transform 100ms ease-out; }
.profile-initial { width: 32px; height: 32px; display: grid; place-items: center; border: 1px solid #c7c4bc; border-radius: 50%; background: #e5e3dd; color: #50504c; font-size: 12px; font-weight: 650; text-transform: uppercase; }
.profile-trigger b, .profile-trigger small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.profile-trigger b { font-size: 12px; line-height: 1.4; }
.profile-trigger small { margin-top: 2px; color: var(--muted); font-size: 10px; }
.profile-trigger b { display: block; overflow: hidden; font-size: 12px; line-height: 1.4; text-overflow: ellipsis; white-space: nowrap; }
.chevron { color: var(--muted); font-size: 20px; font-weight: 300; }
.sidebar-footer > p { margin: 9px 8px 0; color: var(--muted); font-size: 9px; line-height: 1.5; }
.chat-panel { height: 100%; min-width: 0; min-height: 0; overflow: hidden; display: grid; grid-template-rows: 64px minmax(0, 1fr) auto; background: var(--surface); }
.chat-header { z-index: 2; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 0 28px; border-bottom: 1px solid rgba(120, 118, 111, .18); background: rgba(251, 250, 247, .84); backdrop-filter: saturate(140%) blur(18px); }
.chat-panel { height: 100%; min-width: 0; min-height: 0; overflow: hidden; display: grid; grid-template-columns: minmax(0, 1fr); grid-template-rows: 64px minmax(0, 1fr) auto; background: var(--surface); }
.chat-header { z-index: 2; min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 0 28px; border-bottom: 1px solid rgba(120, 118, 111, .18); background: rgba(251, 250, 247, .84); backdrop-filter: saturate(140%) blur(18px); }
.chat-header strong, .chat-header span { display: block; }
.chat-header strong { max-width: min(560px, 62vw); overflow: hidden; font-size: 13px; font-weight: 680; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap; }
.chat-header span { margin-top: 3px; color: var(--muted); font-size: 10px; }
.status { width: 6px; height: 6px; display: inline-block; margin: 0 6px 1px 0; border-radius: 50%; background: #85847d; }
.status-loading { background: var(--accent); }
.credit-button { min-width: 92px; min-height: 44px; padding: 0 12px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); cursor: pointer; font-size: 12px; font-weight: 650; transition: background-color 120ms ease-out, transform 100ms ease-out; }
.credit-button { min-width: 52px; min-height: 40px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 0 10px; border: 0; border-radius: 9px; background: transparent; color: var(--muted); cursor: pointer; font-size: 13px; font-weight: 650; font-variant-numeric: tabular-nums; transition: background-color 120ms ease-out, transform 100ms ease-out; }
.credit-button span { display: block; margin: 0; color: inherit; font-size: inherit; }
.credit-button .credit-icon { position: relative; width: 15px; height: 15px; flex: 0 0 auto; border: 1.5px solid currentColor; border-radius: 50%; }
.credit-button .credit-icon::after { position: absolute; inset: 4px; border-radius: 50%; background: currentColor; content: ""; }
.conversation { min-width: 0; min-height: 0; overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
.conversation.is-empty { display: grid; place-items: center; padding: 28px; }
@@ -103,16 +105,16 @@ button:disabled { cursor: default; opacity: .46; }
.thinking i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); animation: pulse 850ms ease-in-out infinite alternate; }
.thinking i:nth-child(2) { animation-delay: 100ms; }
.thinking i:nth-child(3) { animation-delay: 200ms; }
.suggestion-list { width: 100%; display: grid; margin-top: 12px; border-top: 1px solid var(--line); }
.suggestion-list button { min-height: 44px; display: flex; align-items: center; justify-content: space-between; gap: 14px; padding: 8px 0; border: 0; border-bottom: 1px solid var(--line); background: transparent; color: #494945; cursor: pointer; font-size: 12px; text-align: left; transition: color 120ms ease-out, transform 100ms ease-out; }
.suggestion-list button span { margin: 0; color: var(--faint); font-size: 15px; font-weight: 400; }
.suggestion-list button:not(:disabled):active { transform: scale(.99); }
.composer-suggestions { width: min(760px, 100%); display: flex; gap: 7px; margin: 0 auto 8px; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x proximity; scrollbar-width: none; touch-action: pan-x; -webkit-overflow-scrolling: touch; }
.composer-suggestions::-webkit-scrollbar { display: none; }
.composer-suggestions button { width: auto; min-height: 36px; flex: 0 0 clamp(168px, 56vw, 220px); overflow: hidden; padding: 0 10px; border: 1px solid var(--line); border-radius: 18px; background: rgba(255, 255, 255, .76); color: #494945; cursor: pointer; font-size: 12px; scroll-snap-align: start; text-overflow: ellipsis; white-space: nowrap; transition: border-color 120ms ease-out, color 120ms ease-out, transform 100ms ease-out; }
.composer-suggestions button:not(:disabled):active { transform: scale(.98); }
.error-message { margin: 12px 0 0; padding: 12px 14px; border-left: 3px solid var(--accent); background: var(--accent-soft); color: #713725; font-size: 12px; line-height: 1.6; }
.composer-wrap { z-index: 2; min-width: 0; padding: 12px 24px 14px; border-top: 1px solid rgba(120, 118, 111, .14); background: rgba(251, 250, 247, .86); backdrop-filter: saturate(140%) blur(18px); }
.composer { width: min(760px, 100%); min-height: 58px; display: flex; align-items: flex-end; gap: 10px; margin: 0 auto; padding: 8px 8px 8px 15px; border: 1px solid var(--line-strong); border-radius: 16px; background: rgba(255, 255, 255, .76); transition: border-color 140ms ease-out, box-shadow 140ms ease-out; }
.composer:focus-within { border-color: #8c675a; box-shadow: 0 0 0 3px rgba(133, 67, 47, .1); }
.composer textarea { min-height: 40px; max-height: 128px; flex: 1; resize: none; padding: 10px 0 7px; border: 0; outline: 0; background: transparent; color: var(--ink); font-size: 14px; line-height: 1.45; }
.composer textarea { min-width: 0; min-height: 40px; max-height: 128px; flex: 1; resize: none; padding: 10px 0 7px; border: 0; outline: 0; background: transparent; color: var(--ink); font-size: 14px; line-height: 1.45; }
.composer textarea::placeholder { color: var(--faint); }
.composer button { width: 44px; height: 44px; flex: 0 0 auto; border: 0; border-radius: 12px; background: var(--ink); color: white; cursor: pointer; font-size: 20px; transition: background-color 120ms ease-out, transform 100ms ease-out; }
.composer button:not(:disabled):active { transform: scale(.92); }
@@ -220,7 +222,7 @@ input:disabled, select:disabled { background: #eeece7; color: var(--faint); }
.new-chat:not(:disabled):hover, .composer button:not(:disabled):hover, .button-primary:not(:disabled):hover { background: #383836; }
.session-list button:hover, .profile-trigger:hover, .credit-button:hover { background: rgba(255, 255, 255, .48); }
.starter-list button:not(:disabled):hover { background: #f1efea; }
.suggestion-list button:not(:disabled):hover { color: var(--accent); }
.composer-suggestions button:not(:disabled):hover { border-color: var(--line-strong); color: var(--accent); }
.button-secondary:hover, .dialog-close:hover { background: #eceae5; }
.section-toggle:hover { color: var(--accent); }
}
@@ -230,20 +232,24 @@ input:disabled, select:disabled { background: #eeece7; color: var(--faint); }
}
@media (max-width: 760px) {
.chat-app { grid-template-columns: 1fr; grid-template-rows: 146px minmax(0, 1fr); }
.sidebar { position: relative; height: 146px; padding: 8px 10px; border-right: 0; border-bottom: 1px solid rgba(120, 118, 111, .22); }
.brand-row { height: 40px; padding: 0 4px; }.brand-mark { width: 26px; height: 26px; }
.new-chat { position: absolute; top: 7px; right: 10px; width: auto; min-height: 40px; margin: 0; padding: 0 12px; }.new-chat span { display: none; }
.session-nav { margin-top: 5px; overflow: visible; }.sidebar-label { display: none; }
.session-list { overflow-x: auto; overflow-y: hidden; flex-direction: row; gap: 6px; padding-bottom: 2px; }
.session-list button { width: 146px; min-width: 146px; min-height: 46px; }
.sidebar-footer { position: absolute; top: 7px; right: 104px; margin: 0; padding: 0; border: 0; }
.profile-trigger { width: 44px; min-height: 40px; grid-template-columns: 32px; padding: 4px 6px; }.profile-trigger > span:nth-child(2), .profile-trigger .chevron, .sidebar-footer > p { display: none; }
.chat-panel { grid-template-rows: 58px minmax(0, 1fr) auto; }.chat-header { padding: 0 14px; }.chat-header strong { max-width: 54vw; }.credit-button { min-width: 80px; padding: 0 6px; }
.conversation.is-empty { padding: 20px; }.welcome { padding: 20px 0 30px; }.welcome h1 { font-size: 30px; }.welcome-copy { margin-bottom: 20px; }.onboarding-card, .welcome > .starter-list, .starter-loading { margin-left: 0; }.onboarding-card { padding: 16px; }
.chat-app { grid-template-columns: 1fr; }
.sidebar { position: fixed; z-index: 12; inset: 0 auto 0 0; width: min(86vw, 320px); height: 100dvh; padding: max(12px, env(safe-area-inset-top)) 12px max(12px, env(safe-area-inset-bottom)); border-right: 1px solid rgba(120, 118, 111, .26); background: #ebe9e3; box-shadow: 18px 0 50px rgba(30, 29, 26, .16); visibility: hidden; transform: translateX(-100%); transition: transform 180ms var(--ease-out), visibility 0s linear 180ms; }
.sidebar-open .sidebar { visibility: visible; transform: translateX(0); transition-delay: 0s; }
.sidebar-backdrop { position: fixed; z-index: 11; inset: 0 0 0 min(86vw, 320px); display: block; border: 0; background: rgba(20, 20, 18, .32); opacity: 0; visibility: hidden; transition: opacity 180ms ease-out, visibility 0s linear 180ms; }
.sidebar-open .sidebar-backdrop { opacity: 1; visibility: visible; transition-delay: 0s; }
.brand-row { height: 44px; padding: 0 4px; }.brand-mark { width: 26px; height: 26px; }
.sidebar-close { width: 40px; height: 40px; display: grid; place-items: center; margin-left: auto; border: 0; border-radius: 50%; background: transparent; cursor: pointer; font-size: 22px; }
.new-chat { margin: 14px 0 20px; }
.session-nav { overflow: hidden; }.sidebar-label { display: block; }
.session-list { overflow-x: hidden; overflow-y: auto; flex-direction: column; gap: 2px; padding: 0; }
.session-list button { width: 100%; min-width: 0; min-height: 48px; }
.sidebar-footer { position: static; margin-top: 12px; padding-top: 10px; border-top: 1px solid rgba(120, 118, 111, .22); }
.profile-trigger { width: 100%; min-height: 52px; grid-template-columns: 34px minmax(0, 1fr) 18px; padding: 5px 7px; }.profile-trigger > span:nth-child(2), .profile-trigger .chevron, .sidebar-footer > p { display: block; }
.chat-panel { grid-template-rows: calc(58px + env(safe-area-inset-top)) minmax(0, 1fr) auto; }.chat-header { justify-content: initial; gap: 10px; padding: max(0px, env(safe-area-inset-top)) 10px 0; }.mobile-menu { width: 44px; height: 44px; flex: 0 0 auto; display: grid; place-items: center; border: 0; border-radius: 9px; background: transparent; cursor: pointer; font-size: 20px; }.chat-header > div { min-width: 0; flex: 1; }.chat-header strong { max-width: 100%; }.credit-button { min-width: 52px; padding: 0 8px; }
.conversation.is-empty { padding: 20px 16px; }.welcome { padding: 20px 0 30px; }.welcome h1 { font-size: 30px; }.welcome-copy { margin-bottom: 20px; }.onboarding-card, .welcome > .starter-list, .starter-loading { margin-left: 0; }.onboarding-card { padding: 16px; }
.starter-list button { min-height: 62px; }.starter-content span { font-size: 12px; }
.message-list { width: 100%; padding: 12px 10px 46px; }.message { padding: 5px 0; }.message-content { max-width: 86%; }.error-message { margin-left: 0; }
.composer-wrap { padding: 9px 10px 10px; }.composer-wrap > p { display: none; }
.message-list { width: 100%; padding: 12px 14px 34px; }.message { padding: 5px 0; }.message-content { max-width: 88%; }.message-assistant .message-content { max-width: 100%; }.error-message { margin-left: 0; }
.composer-wrap { padding: 8px 10px max(10px, env(safe-area-inset-bottom)); }.composer { min-height: 52px; border-radius: 24px; }.composer-wrap > p { display: none; }
.profile-overlay { align-items: flex-end; }.profile-dialog { width: 100%; height: min(88dvh, 760px); padding: 20px; border-top: 1px solid var(--line); border-left: 0; border-radius: 16px 16px 0 0; transform: translateY(20px); }.profile-overlay.is-open .profile-dialog { transform: translateY(0); }
.account-actions { padding-bottom: max(0px, env(safe-area-inset-bottom)); }
.admin-header { padding: 0 16px; }.admin-scroll { padding: 24px 16px 50px; }.section-title { align-items: stretch; }.generated-section .section-title { flex-direction: column; }.generated-section .button-secondary { align-self: flex-start; }
+1 -1
View File
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
title: "Ayanam · 印度占星对话",
title: "Jyotisha · 印度占星对话",
description: "与 Mastra Agent 对话,基于星盘证据讨论事业、关系与时间窗口。",
};
+4 -4
View File
@@ -71,10 +71,10 @@ export default function LoginPage() {
return (
<main className="standalone-page auth-page">
<section className="auth-panel" aria-labelledby="login-title">
<div className="auth-brand"><span aria-hidden="true"></span><strong>Ayanam</strong></div>
<p className="page-eyebrow"></p>
<h1 id="login-title"></h1>
<p className="page-intro"></p>
<div className="auth-brand"><span aria-hidden="true"></span><strong>Jyotisha</strong></div>
<p className="page-eyebrow"></p>
<h1 id="login-title"> Jyotisha</h1>
<p className="page-intro">使</p>
{!sent ? (
<form className="stack-form" onSubmit={sendOtp}>
+40 -21
View File
@@ -33,7 +33,7 @@ const themes: Array<{ id: Exclude<Theme, "general">; label: string; prompt: stri
{ id: "timing", label: "时运", prompt: "未来十二个月有哪些重要时间窗口?" },
];
const presetOnboardingMessage = "你好,我是 Ayanam。开始前,我想先认识你。请问我该怎么称呼你?";
const presetOnboardingMessage = "你好,我是 Jyotisha。开始前,我想先认识你。请问我该怎么称呼你?";
const emptyProfile: Profile = {
name: "",
@@ -276,7 +276,7 @@ function ProfileFields({ value, onChange }: { value: Profile; onChange: (profile
function OnboardingChatMessage({ role, text, streaming = false, length = text.length }: { role: Message["role"]; text: string; streaming?: boolean; length?: number }) {
const visibleText = streaming ? text.slice(0, length) : text;
return (
<article className={`message message-${role} onboarding-message`} aria-label={role === "assistant" ? "Ayanam" : "你"}>
<article className={`message message-${role} onboarding-message`} aria-label={role === "assistant" ? "Jyotisha" : "你"}>
<div className="message-content">
<div className="message-bubble">
{role === "assistant" ? (
@@ -330,6 +330,7 @@ export default function Home() {
const [profile, setProfile] = useState<Profile>(emptyProfile);
const [profileDraft, setProfileDraft] = useState<Profile>(emptyProfile);
const [profileOpen, setProfileOpen] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [profileNotice, setProfileNotice] = useState("");
const [account, setAccount] = useState<Account | null>(null);
const [accountError, setAccountError] = useState("");
@@ -355,6 +356,7 @@ export default function Home() {
const [presetMessageLength, setPresetMessageLength] = useState(0);
const conversationEnd = useRef<HTMLDivElement>(null);
const accountTrigger = useRef<HTMLButtonElement>(null);
const mobileMenuTrigger = useRef<HTMLButtonElement>(null);
const closeButton = useRef<HTMLButtonElement>(null);
const redeemInput = useRef<HTMLInputElement>(null);
const composerInput = useRef<HTMLTextAreaElement>(null);
@@ -363,6 +365,7 @@ export default function Home() {
const activeError = requestError && requestError.sessionId === activeSession?.id ? requestError.message : "";
const isLoading = pendingSessionId === activeSession?.id;
const activeStreamingText = streamingReply && streamingReply.sessionId === activeSession?.id ? streamingReply.text : "";
const activeSuggestions = activeSession?.messages.reduce((latest, message) => message.role === "assistant" && message.suggestions?.length ? message.suggestions : latest, [] as string[]) ?? [];
const accountId = account?.user.id;
const profileComplete = isProfileComplete(profile);
const onboardingPending = profileComplete && !onboarding && !onboardingError;
@@ -502,6 +505,18 @@ export default function Home() {
}
}, [onboardingStep, presetMessageFinished, profileComplete, profileOpen]);
useEffect(() => {
if (!mobileSidebarOpen) return;
const closeOnEscape = (event: globalThis.KeyboardEvent) => {
if (event.key === "Escape") {
setMobileSidebarOpen(false);
window.requestAnimationFrame(() => mobileMenuTrigger.current?.focus());
}
};
window.addEventListener("keydown", closeOnEscape);
return () => window.removeEventListener("keydown", closeOnEscape);
}, [mobileSidebarOpen]);
useEffect(() => {
if (!profileOpen) return;
(redeemOpen ? redeemInput.current : closeButton.current)?.focus();
@@ -557,6 +572,7 @@ export default function Home() {
async function startNewChat() {
if (!account || creatingSession) return;
setMobileSidebarOpen(false);
const nextSession = createSession();
const previousSessionId = activeSession?.id ?? "";
setCreatingSession(true);
@@ -884,9 +900,10 @@ export default function Home() {
}
return (
<main className="chat-app">
<aside className="sidebar" aria-label="对话导航">
<div className="brand-row"><span className="brand-mark" aria-hidden="true"></span><strong>Ayanam</strong></div>
<main className={`chat-app ${mobileSidebarOpen ? "sidebar-open" : ""}`}>
<button className="sidebar-backdrop" aria-label="关闭聊天记录" type="button" onClick={() => setMobileSidebarOpen(false)} />
<aside className="sidebar" id="chat-sidebar" aria-label="对话导航">
<div className="brand-row"><span className="brand-mark" aria-hidden="true"></span><strong>Jyotisha</strong><button className="sidebar-close" aria-label="关闭聊天记录" type="button" onClick={() => setMobileSidebarOpen(false)}>×</button></div>
<button className="new-chat" type="button" onClick={() => void startNewChat()} disabled={!hydrated || !account || creatingSession || Boolean(pendingSessionId)}><span aria-hidden="true"></span> {creatingSession ? "正在创建" : "新对话"}</button>
<nav className="session-nav" aria-label="聊天记录">
<span className="sidebar-label"></span>
@@ -896,7 +913,7 @@ export default function Home() {
className={session.id === activeSession?.id ? "is-active" : ""}
key={session.id}
type="button"
onClick={() => { setActiveSessionId(session.id); setDraft(""); }}
onClick={() => { setActiveSessionId(session.id); setDraft(""); setMobileSidebarOpen(false); }}
aria-current={session.id === activeSession?.id ? "page" : undefined}
>
<span>{session.title}</span>
@@ -908,7 +925,7 @@ export default function Home() {
<div className="sidebar-footer">
<button className="profile-trigger" ref={accountTrigger} type="button" onClick={() => openAccount()}>
<span className="profile-initial" aria-hidden="true">{profile.name.trim().slice(0, 1) || account?.user.email?.slice(0, 1).toUpperCase() || "你"}</span>
<span><b>{profile.name.trim() || account?.user.email || "账户"}</b><small>{account ? `${account.credits} 点余额` : accountError || "正在读取账户"}</small></span>
<span><b>{profile.name.trim() || account?.user.email || "账户"}</b></span>
<span className="chevron" aria-hidden="true"></span>
</button>
<p></p>
@@ -917,12 +934,14 @@ export default function Home() {
<section className="chat-panel">
<header className="chat-header">
<button className="mobile-menu" ref={mobileMenuTrigger} aria-label="打开聊天记录" aria-controls="chat-sidebar" aria-expanded={mobileSidebarOpen} type="button" onClick={() => setMobileSidebarOpen(true)}><span aria-hidden="true"></span></button>
<div>
<strong>{activeSession?.title || "新对话"}</strong>
<span><i className={`status ${isLoading ? "status-loading" : "status-idle"}`} />{isLoading ? (activeStreamingText ? "正在回答" : "正在核对星盘信息") : "基于星盘证据回答"}</span>
</div>
<button className="credit-button" type="button" onClick={() => openAccount(account?.credits === 0)} aria-label="打开账户与兑换码">
{account ? `剩余 ${account.credits}` : accountError || "读取余额中"}
<button className="credit-button" type="button" onClick={() => openAccount(account?.credits === 0)} aria-label={account ? `余额 ${account.credits} 点,打开账户与兑换码` : accountError || "读取余额中"}>
<span className="credit-icon" aria-hidden="true" />
<span>{account ? account.credits : "—"}</span>
</button>
</header>
@@ -977,7 +996,7 @@ export default function Home() {
{profileComplete && presetMessageFinished && (onboardingPending ? (
<div className="starter-loading" role="status"></div>
) : (
<div className="starter-list" aria-label="Ayanam 推荐的初始问题">
<div className="starter-list" aria-label="Jyotisha 推荐的初始问题">
{(onboarding?.suggestions ?? themes.map((item) => ({ theme: item.id, text: item.prompt }))).map((item, index) => {
const theme = themes.find((candidate) => candidate.id === item.theme);
return (
@@ -995,25 +1014,18 @@ export default function Home() {
</div>
) : (
<div className="message-list" aria-busy={isLoading}>
<span className="sr-only" aria-live="polite">{isLoading ? "Ayanam 正在回答" : ""}</span>
<span className="sr-only" aria-live="polite">{isLoading ? "Jyotisha 正在回答" : ""}</span>
{activeSession.messages.map((message, index) => (
<article className={`message message-${message.role}`} key={`${message.role}-${index}`} aria-label={message.role === "assistant" ? "Ayanam" : "你"}>
<article className={`message message-${message.role}`} key={`${message.role}-${index}`} aria-label={message.role === "assistant" ? "Jyotisha" : "你"}>
<div className="message-content">
<div className="message-bubble">
{message.role === "assistant" ? <ChatMessageContent text={message.text} /> : <p>{message.text}</p>}
</div>
{message.role === "assistant" && index === activeSession.messages.length - 1 && message.suggestions && message.suggestions.length > 0 && (
<div className="suggestion-list" aria-label="推荐继续提问">
{message.suggestions.map((question) => (
<button key={question} type="button" disabled={Boolean(pendingSessionId) || !account} onClick={() => void send(question)}>{question}<span aria-hidden="true"></span></button>
))}
</div>
)}
</div>
</article>
))}
{isLoading && (
<article className="message message-assistant" aria-label={activeStreamingText ? "Ayanam 正在回答" : "Ayanam 正在分析"}>
<article className="message message-assistant" aria-label={activeStreamingText ? "Jyotisha 正在回答" : "Jyotisha 正在分析"}>
<div className="message-content">
<div className="message-bubble">
{activeStreamingText ? <ChatMessageContent text={activeStreamingText} /> : <div className="thinking"><i /><i /><i /></div>}
@@ -1028,6 +1040,13 @@ export default function Home() {
</div>
<div className="composer-wrap">
{activeSuggestions.length > 0 && (
<div className="composer-suggestions" aria-label="推荐继续提问">
{activeSuggestions.map((question) => (
<button key={question} type="button" disabled={Boolean(pendingSessionId) || !account} onClick={() => void send(question)}>{question}</button>
))}
</div>
)}
<form className="composer" onSubmit={submit}>
<textarea
ref={composerInput}
@@ -1036,7 +1055,7 @@ export default function Home() {
? "正在读取账户…"
: !profileComplete
? onboardingStep === "name"
? presetMessageFinished ? "输入你的称呼" : "Ayanam 正在输入…"
? presetMessageFinished ? "输入你的称呼" : "Jyotisha 正在输入…"
: "请先完成上方资料"
: account.credits === 0
? "余额不足,发送时将打开兑换码"
+2 -2
View File
@@ -128,9 +128,9 @@ Do not provide medical, legal, investment, or safety-critical instructions. Do n
export const onboardingAgent = new Agent({
id: "jyotish-onboarding-guide",
name: "Ayanam Onboarding Guide",
name: "Jyotisha Onboarding Guide",
model: languageModelSettings.model,
instructions: `You create the first conversational turn for Ayanam, a Vedic astrology chat product.
instructions: `You create the first conversational turn for Jyotisha, a Vedic astrology chat product.
Load and follow the jyotish-vedic-astrology skill so the suggested questions respect its scope and truth boundaries.
This is onboarding, not a chart reading: do not calculate, infer, or claim placements, timing windows, personality traits, relationship outcomes, or career conclusions.
Return valid JSON only. Do not use Markdown fences, commentary, or hidden fields.