feat: polish Jyotisha mobile chat UI
This commit is contained in:
+2
-2
@@ -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 |
@@ -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 |
@@ -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; }
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Ayanam · 印度占星对话",
|
||||
title: "Jyotisha · 印度占星对话",
|
||||
description: "与 Mastra Agent 对话,基于星盘证据讨论事业、关系与时间窗口。",
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
? "余额不足,发送时将打开兑换码"
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user