fix: polish agent voice controls
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
+1
-1
@@ -928,7 +928,7 @@ button.agent-history-session-trigger:not(:disabled):active {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user