feat(web): prominent agent launcher (FAB, Ctrl/Cmd+J, sidebar entry, quick goals)
- Bottom-right FAB (56px, prism ring) with active-run count, pulse and latest status summary; turns into "collapse" beside the open drawer; hidden on the Agent page and while the mini dock shows; lifts above bottom floating bars - Ctrl/Cmd+J toggles the drawer, Esc closes; input is focused on open - Emphasized Agent entry at the top of the sidebar (expanded and collapsed) with active-run count; header button kept only below xl - Drawer first screen offers 3 page-aware quick goals that fill the input - Respects reduced motion; AgentShortcutsHelp exported for the settings page Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
16ab2a6c84
commit
39a001c099
7 files changed
+433
-8
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { EventType } from "@umt/contracts";
|
||||
import { FlaskConical, Play, RefreshCw, WifiOff } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { FlaskConical, Play, RefreshCw, Sparkles, WifiOff } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type AgentMode,
|
||||
@@ -78,6 +78,7 @@ export function GoalComposer({
|
||||
prefix,
|
||||
defaultGoal = "",
|
||||
compact = false,
|
||||
suggestions,
|
||||
}: {
|
||||
onSubmit: (goal: string, mode: AgentMode) => void;
|
||||
pending: boolean;
|
||||
@@ -85,11 +86,23 @@ export function GoalComposer({
|
||||
prefix?: string;
|
||||
defaultGoal?: string;
|
||||
compact?: boolean;
|
||||
/** 快捷目标:点一下填入输入框(不直接提交),光标停在末尾 */
|
||||
suggestions?: string[];
|
||||
}) {
|
||||
const [goal, setGoal] = useState(defaultGoal);
|
||||
const [mode, setMode] = useState<AgentMode>("execute");
|
||||
const box = useRef<HTMLDivElement>(null);
|
||||
const fill = (text: string) => {
|
||||
setGoal(text);
|
||||
requestAnimationFrame(() => {
|
||||
const ta = box.current?.querySelector("textarea");
|
||||
if (!ta) return;
|
||||
ta.focus();
|
||||
ta.setSelectionRange(text.length, text.length);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<div ref={box} className="space-y-1.5">
|
||||
<PromptInput
|
||||
value={goal}
|
||||
onValueChange={setGoal}
|
||||
@@ -116,6 +129,27 @@ export function GoalComposer({
|
||||
<p className="px-1 text-meta text-muted-foreground" aria-live="polite">
|
||||
{MODE_HELP[mode].help} Enter 发送,Shift+Enter 换行。
|
||||
</p>
|
||||
{suggestions?.length ? (
|
||||
<div className="pt-2">
|
||||
<p className="mb-1.5 flex items-center gap-1.5 px-1 text-meta text-muted-foreground">
|
||||
<Sparkles aria-hidden className="size-3.5" />
|
||||
试试这些
|
||||
</p>
|
||||
<ul aria-label="快捷目标" className="flex flex-col gap-1.5">
|
||||
{suggestions.map((s) => (
|
||||
<li key={s}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fill(s)}
|
||||
className="w-full cursor-pointer rounded-xl border border-border/70 px-3 py-2 text-left text-caption text-foreground/90 outline-none transition-colors duration-micro hover:bg-foreground/[0.04] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{s.endsWith(" ") || s.endsWith(":") ? `${s}…` : s}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user