feat: show agent activity and simplify starter view
This commit is contained in:
Generated
+11
@@ -29,6 +29,7 @@
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"thinking-orbs": "^0.1.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
@@ -10834,6 +10835,16 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/thinking-orbs": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/thinking-orbs/-/thinking-orbs-0.1.1.tgz",
|
||||
"integrity": "sha512-nLvLTGJtk74K13MmP7XiRdzFiQx7UsoZAIyBZNgXyl7Q3h2mdz0r3eKn9LCsuzb3DGOVjwDvMhtnAkIqJJRVQA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-dom": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"thinking-orbs": "^0.1.1",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
|
||||
@@ -164,9 +164,9 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.message-markdown p { margin: 0 0 14px; }
|
||||
.message-markdown ul, .message-markdown ol { margin: 10px 0 16px; padding-left: 24px; }
|
||||
.message-markdown li { margin: 6px 0; }
|
||||
.thinking { height: 24px; display: flex; align-items: center; gap: 5px; }
|
||||
.thinking i:nth-child(2) { animation-delay: 100ms; }
|
||||
.thinking i:nth-child(3) { animation-delay: 200ms; }
|
||||
.agent-activity-status { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-body-sm); line-height: 1.5; }
|
||||
.agent-activity-status canvas { flex: 0 0 auto; }
|
||||
.agent-activity-status + .message-markdown { margin-top: var(--space-2); }
|
||||
.composer-suggestions::-webkit-scrollbar { display: none; }
|
||||
.composer textarea::placeholder { color: var(--color-ink-tertiary); }
|
||||
.composer button svg { width: 19px; height: 19px; }
|
||||
@@ -542,14 +542,6 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.product-entrypoint-action .starter-arrow { width: 15px; height: 15px; color: currentColor; }
|
||||
.starter-loading { color: var(--color-ink-secondary); margin-left: 0; padding: var(--space-5); border-radius: var(--radius-lg); background: var(--color-canvas-muted); font-size: 14px; }
|
||||
.starter-note { margin: 10px 0 0; color: var(--color-ink-secondary); line-height: 1.5; grid-column: 1 / -1; font-size: 13px; }
|
||||
.parameter-freeze-panel { display: grid; grid-column: 1 / -1; gap: var(--space-3); padding: var(--space-4); border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-canvas); }
|
||||
.parameter-freeze-panel > div { display: flex; flex-wrap: wrap; justify-content: space-between; gap: var(--space-2); color: var(--color-ink-secondary); font-size: var(--type-caption); }
|
||||
.parameter-freeze-panel b { color: var(--color-ink); font-size: var(--type-title-sm); }
|
||||
.parameter-freeze-panel dl { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: var(--space-2); margin: 0; }
|
||||
.parameter-freeze-panel dl > div { min-width: 0; padding: var(--space-2); border-radius: var(--radius-md); background: var(--color-canvas-soft); }
|
||||
.parameter-freeze-panel dt { color: var(--color-ink-tertiary); font-size: 11px; }
|
||||
.parameter-freeze-panel dd { overflow: hidden; margin: 2px 0 0; color: var(--color-ink); text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||
|
||||
.message-list { margin: 0 auto; width: min(900px, 100%); padding: var(--space-8) var(--space-8) var(--space-16); }
|
||||
.message { display: flex; animation: message-enter 160ms var(--ease-out) both; padding: var(--space-2) 0; }
|
||||
.agent-avatar { width: 32px; height: 32px; display: block; flex: 0 0 32px; margin-top: var(--space-2); border-radius: 50%; background: var(--color-canvas) url("/jyotish-logo.png") center / contain no-repeat; box-shadow: 0 0 0 1px var(--color-border); }
|
||||
@@ -565,7 +557,6 @@ button:disabled { cursor: default; opacity: .45; }
|
||||
.message-markdown strong { color: var(--color-ink); font-weight: 500; }
|
||||
.message-markdown a { text-underline-offset: 3px; color: var(--color-action); }
|
||||
.message-markdown code { padding: 2px 5px; font-size: .9em; border-radius: var(--radius-xs); background: var(--color-canvas-muted); font-family: var(--font-mono); }
|
||||
.thinking i { width: 5px; height: 5px; border-radius: 50%; animation: pulse 850ms ease-in-out infinite alternate; background: var(--color-action); }
|
||||
.error-message { margin: 12px 0 0; padding: 12px 14px; border-left: 3px solid var(--color-danger); background: var(--color-danger-muted); color: var(--color-danger); font-size: 13px; line-height: 1.6; border-color: var(--color-danger); border-radius: 0 var(--radius-md) var(--radius-md) 0; }
|
||||
|
||||
.composer-wrap { position: sticky; bottom: 0; z-index: 2; min-width: 0; border-top: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: var(--space-3) var(--space-6) var(--space-4); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); }
|
||||
|
||||
@@ -15,7 +15,6 @@ import { ConversationalBirthTimeRectification } from "@/components/conversationa
|
||||
import { ChatMessageContent } from "@/components/chat-message-content";
|
||||
import { AgentAvatar, ChatMessageRow } from "@/components/chat-message-row";
|
||||
import { ModelSelector } from "@/components/model-selector";
|
||||
import { ParameterFreezePanel, type ParameterFreezeRow } from "@/components/parameter-freeze-panel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -976,16 +975,6 @@ export default function Home() {
|
||||
|
||||
const profileComplete = isProfileComplete(profile);
|
||||
const birthTimeDisplay = birthTimeDisplayState(profile);
|
||||
const profileBirthPlace = selectedBirthPlace(profile);
|
||||
const parameterFreezeRows: ParameterFreezeRow[] = profileComplete && profileBirthPlace ? [
|
||||
{ label: "出生时间", value: `${profile.date} ${profile.time || "未定"}` },
|
||||
{ label: "出生地点", value: profileBirthPlace.label },
|
||||
{ label: "经纬度", value: `${profileBirthPlace.lat.toFixed(4)}, ${profileBirthPlace.lon.toFixed(4)}` },
|
||||
{ label: "时区", value: `UTC+${profileBirthPlace.tz}` },
|
||||
{ label: "Ayanamsa", value: "Lahiri / Sidereal" },
|
||||
{ label: "Node mode", value: "True Node" },
|
||||
{ label: "出生时间精度", value: birthTimeDisplay?.kind === "candidate" ? "候选时间" : birthTimeDisplay?.kind === "confirmed" ? "已确认" : "待校正" },
|
||||
] : [];
|
||||
const dailyStarlanguage = dailyStarlanguageCard ?? (profileComplete ? buildDailyStarlanguageCard(profile) : null);
|
||||
const onboardingPending = profileComplete && !onboarding && !onboardingError;
|
||||
const currentOnboardingMessage = onboardingJustCompleted
|
||||
@@ -2915,7 +2904,6 @@ export default function Home() {
|
||||
<div className="starter-loading" role="status">正在准备三个入门问题…</div>
|
||||
) : (
|
||||
<div className="starter-list" aria-label="Jyotisha 推荐的初始问题">
|
||||
{parameterFreezeRows.length > 0 && <ParameterFreezePanel rows={parameterFreezeRows} />}
|
||||
<div className="product-entrypoints" aria-label="常用占星入口">
|
||||
<article className="daily-starlanguage-card product-entrypoint-card" aria-label="今日星语">
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ThinkingOrb, type OrbState } from "thinking-orbs";
|
||||
|
||||
const labels = {
|
||||
working: "正在处理任务…",
|
||||
searching: "正在搜索相关信息…",
|
||||
solving: "正在分析问题…",
|
||||
listening: "正在聆听…",
|
||||
composing: "正在组织回答…",
|
||||
shaping: "正在生成结果…",
|
||||
} as const satisfies Record<OrbState, string>;
|
||||
|
||||
export function AgentActivityStatus({
|
||||
state,
|
||||
label = labels[state],
|
||||
}: Readonly<{
|
||||
state: OrbState;
|
||||
label?: string;
|
||||
}>) {
|
||||
return (
|
||||
<div className="agent-activity-status" role="status">
|
||||
<ThinkingOrb aria-hidden="true" state={state} size={20} />
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AgentActivityStatus } from "@/components/agent-activity-status";
|
||||
import { ChatMessageContent } from "@/components/chat-message-content";
|
||||
import type { ChatMessageView } from "@/lib/chat-message-view";
|
||||
|
||||
@@ -22,8 +23,11 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView
|
||||
<div className="message-bubble">
|
||||
{message.role === "assistant" ? (
|
||||
message.state === "thinking"
|
||||
? <div className="thinking"><i /><i /><i /></div>
|
||||
: <ChatMessageContent text={message.text} />
|
||||
? <AgentActivityStatus state="working" label="正在核对星盘信息…" />
|
||||
: <>
|
||||
{message.state === "streaming" && <AgentActivityStatus state="composing" />}
|
||||
<ChatMessageContent text={message.text} />
|
||||
</>
|
||||
) : <p>{message.text}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
export type ParameterFreezeRow = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export function ParameterFreezePanel({ rows }: { readonly rows: readonly ParameterFreezeRow[] }) {
|
||||
return (
|
||||
<section className="parameter-freeze-panel" aria-label="当前排盘参数冻结">
|
||||
<div>
|
||||
<b>当前排盘参数</b>
|
||||
<span>这些参数会随咨询一起送入证据链。</span>
|
||||
</div>
|
||||
<dl>
|
||||
{rows.map((row) => (
|
||||
<div key={row.label}>
|
||||
<dt>{row.label}</dt>
|
||||
<dd>{row.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import { chatMessageViews } from "../src/lib/chat-message-view.ts";
|
||||
|
||||
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
const messageRowSource = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8");
|
||||
const activitySource = readFileSync(new URL("../src/components/agent-activity-status.tsx", import.meta.url), "utf8");
|
||||
|
||||
const previousMessages = [
|
||||
{ role: "user", text: "问题" },
|
||||
@@ -42,6 +44,14 @@ test("does not duplicate a completed assistant answer while loading state settle
|
||||
assert.equal(views.at(-1)?.state, "settled");
|
||||
});
|
||||
|
||||
test("shows honest agent activity states before and during streamed text", () => {
|
||||
assert.match(messageRowSource, /state="working" label="正在核对星盘信息…"/);
|
||||
assert.match(messageRowSource, /message\.state === "streaming" && <AgentActivityStatus state="composing"/);
|
||||
assert.match(activitySource, /<ThinkingOrb aria-hidden="true" state=\{state\} size=\{20\}/);
|
||||
assert.match(activitySource, /satisfies Record<OrbState, string>/);
|
||||
assert.doesNotMatch(globalStyles, /\.thinking\b/);
|
||||
});
|
||||
|
||||
test("keeps the suggestion row height stable while an answer streams", () => {
|
||||
// Given: a completed answer already supplies follow-up suggestions.
|
||||
const suggestionBlock = pageSource.match(/\{activeSuggestions\.length > 0[\s\S]*?<div className="composer-suggestions"[\s\S]*?<\/div>\n\s*\)\}/);
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
const panelSource = readFileSync(new URL("../src/components/parameter-freeze-panel.tsx", import.meta.url), "utf8");
|
||||
const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
const panelPath = new URL("../src/components/parameter-freeze-panel.tsx", import.meta.url);
|
||||
|
||||
test("starter workbench shows a parameter freeze panel when profile is complete", () => {
|
||||
assert.match(pageSource, /ParameterFreezePanel/);
|
||||
assert.match(pageSource, /parameterFreezeRows/);
|
||||
assert.match(pageSource, /Ayanamsa/);
|
||||
assert.match(pageSource, /Node mode/);
|
||||
assert.match(pageSource, /出生时间精度/);
|
||||
assert.match(panelSource, /当前排盘参数冻结/);
|
||||
assert.match(panelSource, /这些参数会随咨询一起送入证据链/);
|
||||
assert.match(globalStyles, /\.parameter-freeze-panel/);
|
||||
test("starter workbench hides internal chart parameters from users", () => {
|
||||
assert.doesNotMatch(pageSource, /ParameterFreezePanel|parameterFreezeRows|当前排盘参数/);
|
||||
assert.doesNotMatch(globalStyles, /\.parameter-freeze-panel/);
|
||||
assert.equal(existsSync(panelPath), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user