Compare commits

...
2 Commits
Author SHA1 Message Date
jiang 2c35f500df fix: polish agent voice controls 2026-07-28 17:45:55 +08:00
jiang 3ba62ceacb fix: remove history item click outline 2026-07-28 17:41:04 +08:00
5 changed files with 90 additions and 9 deletions
@@ -610,7 +610,13 @@ export function AgentCommandPanel({
status={streaming ? "streaming" : "ready"} status={streaming ? "streaming" : "ready"}
onStop={onStopPrompt} onStop={onStopPrompt}
> >
{streaming ? undefined : <SendHorizontal size={15} aria-hidden="true" />} {streaming ? undefined : (
<SendHorizontal
size={15}
style={{ transform: "rotate(-45deg)" }}
aria-hidden="true"
/>
)}
</PromptInputSubmit> </PromptInputSubmit>
</div> </div>
</PromptInputFooter> </PromptInputFooter>
@@ -650,13 +656,13 @@ function VoiceInputButton({
title={label} title={label}
className={cn( className={cn(
"group relative grid h-8 w-8 shrink-0 place-items-center overflow-visible rounded-lg border after:absolute after:-inset-1 after:content-['']", "group relative grid h-8 w-8 shrink-0 place-items-center overflow-visible rounded-lg border after:absolute after:-inset-1 after:content-['']",
"border-transparent bg-transparent text-blue-600 shadow-none", "border-transparent bg-transparent shadow-none",
"transition-[color,background-color,border-color,box-shadow,transform] duration-200", "transition-[color,background-color,border-color,box-shadow,transform] duration-200",
"hover:bg-slate-50 hover:text-blue-700",
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600/25 focus-visible:ring-offset-1", "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600/25 focus-visible:ring-offset-1",
"disabled:cursor-not-allowed disabled:opacity-45", "disabled:cursor-not-allowed disabled:opacity-45",
isListening && isListening
"status-tone-danger bg-[var(--status-soft)] text-[var(--status-foreground)] [@media(hover:hover)]:hover:brightness-95" ? "status-tone-danger bg-[var(--status-soft)] text-[var(--status-foreground)] [@media(hover:hover)]:hover:border-[var(--status-border)] [@media(hover:hover)]:hover:brightness-95"
: "text-slate-600 [@media(hover:hover)]:hover:border-[var(--action-border-hover)] [@media(hover:hover)]:hover:bg-[var(--action-soft)] [@media(hover:hover)]:hover:text-[var(--action-blue-hover)]"
)} )}
disabled={disabled} disabled={disabled}
onClick={onClick} onClick={onClick}
@@ -668,10 +674,19 @@ function VoiceInputButton({
aria-hidden="true" aria-hidden="true"
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.9 }}
animate={ animate={
reduceMotion ? { opacity: 0.38, scale: 1 } : { opacity: [0.4, 0], scale: [1, 1.28] } reduceMotion
? { opacity: 0.38, scale: 1 }
: { opacity: [0, 0.4, 0], scale: [0.9, 1, 1.28] }
} }
transition={ transition={
reduceMotion ? { duration: 0.16 } : { duration: 1.7, ease: "easeOut", repeat: Infinity } reduceMotion
? { duration: 0.16 }
: {
duration: 1.7,
ease: "easeOut",
repeat: Infinity,
times: [0, 0.12, 1]
}
} }
/> />
) : null} ) : null}
@@ -187,7 +187,7 @@ export function AgentHistoryPanel({
<motion.button <motion.button
key="session-summary" key="session-summary"
type="button" type="button"
className="min-w-0 text-left" className="agent-history-session-trigger min-w-0 text-left"
disabled={itemLoading || deleting} disabled={itemLoading || deleting}
initial="initial" initial="initial"
animate="animate" animate="animate"
+5 -1
View File
@@ -761,6 +761,10 @@ button:disabled {
background-color: var(--agent-floating-reading-active); background-color: var(--agent-floating-reading-active);
} }
button.agent-history-session-trigger:not(:disabled):active {
outline: none;
}
.agent-history-rename-input, .agent-history-rename-input,
.agent-history-rename-input:focus-visible { .agent-history-rename-input:focus-visible {
border-color: #cbd5e1; border-color: #cbd5e1;
@@ -924,7 +928,7 @@ button:disabled {
} }
.agent-panel-primary-icon { .agent-panel-primary-icon {
border: 1px solid var(--action-blue); border: 0;
background: var(--action-blue); background: var(--action-blue);
color: #ffffff; color: #ffffff;
box-shadow: var(--action-shadow-primary); box-shadow: var(--action-shadow-primary);
+17
View File
@@ -134,6 +134,23 @@ test("desktop Agent header expands into one attached acrylic history surface", a
.poll(() => getSurfacePresentation(historyButton)) .poll(() => getSurfacePresentation(historyButton))
.toEqual(historyButtonPresentationBefore); .toEqual(historyButtonPresentationBefore);
const sessionTrigger = history.locator(".agent-history-session-trigger").first();
const sessionTriggerBox = await sessionTrigger.boundingBox();
const historyHeadingBox = await history.getByText("历史记录", { exact: true }).boundingBox();
expect(sessionTriggerBox).not.toBeNull();
expect(historyHeadingBox).not.toBeNull();
await page.mouse.move(
sessionTriggerBox!.x + sessionTriggerBox!.width / 2,
sessionTriggerBox!.y + sessionTriggerBox!.height / 2
);
await page.mouse.down();
await expect(sessionTrigger).toHaveCSS("outline-style", "none");
await page.mouse.move(
historyHeadingBox!.x + historyHeadingBox!.width / 2,
historyHeadingBox!.y + historyHeadingBox!.height / 2
);
await page.mouse.up();
const headerBox = await agentHeader.boundingBox(); const headerBox = await agentHeader.boundingBox();
const agentPanelBox = await agentPanel.boundingBox(); const agentPanelBox = await agentPanel.boundingBox();
const historyBox = await history.boundingBox(); const historyBox = await history.boundingBox();
@@ -83,3 +83,48 @@ test("speech recognition writes final transcript into the prompt", async ({ page
await expect(page.getByPlaceholder("输入调度问题,Agent 将通过后端会话流式响应")).toHaveValue("检查城南泵站水位"); await expect(page.getByPlaceholder("输入调度问题,Agent 将通过后端会话流式响应")).toHaveValue("检查城南泵站水位");
}); });
test("speech recognition pulse stays transparent across its loop boundary", async ({ page }) => {
await page.addInitScript(() => {
Object.defineProperty(window, "SpeechRecognition", {
configurable: true,
value: class SpeechRecognition {
start() {}
stop() {}
abort() {}
}
});
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: "语音输入" }).click();
const pulse = page.locator('[data-slot="voice-input-pulse"]');
await expect(pulse).toBeVisible();
const seamOpacity = await pulse.evaluate((element) => {
const animation = element
.getAnimations()
.find((candidate) =>
(candidate.effect as KeyframeEffect | null)
?.getKeyframes()
.some((keyframe) => keyframe.opacity !== undefined)
);
if (!animation?.effect) {
throw new Error("Voice input pulse opacity animation was not found");
}
animation.pause();
const duration = Number(animation.effect.getTiming().duration);
animation.currentTime = duration - 1;
const before = Number(window.getComputedStyle(element).opacity);
animation.currentTime = duration + 1;
const after = Number(window.getComputedStyle(element).opacity);
return { before, after };
});
expect(seamOpacity.before).toBeLessThan(0.02);
expect(seamOpacity.after).toBeLessThan(0.02);
});