Skip to content

Commit 002532b

Browse files
committed
feat(webapp): placeholder suggestion with Tab-to-accept; even send/stop geometry
- the blank state's field shows the top suggested prompt as its placeholder; Tab drops it into the field as editable text, never sending it - the send and stop buttons share identical square geometry
1 parent a90981f commit 002532b

2 files changed

Lines changed: 42 additions & 4 deletions

File tree

apps/webapp/app/components/dashboard-agent/DashboardAgentComposer.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function DashboardAgentComposer({
2020
context,
2121
layout = "docked",
2222
autoFocus = true,
23+
placeholderSuggestion,
2324
}: {
2425
value: string;
2526
onChange: (value: string) => void;
@@ -38,6 +39,12 @@ export function DashboardAgentComposer({
3839
// Off only where a composer isn't the thing the user came for — the storybook
3940
// gallery renders several at once and must not steal the page's focus.
4041
autoFocus?: boolean;
42+
/**
43+
* A suggested prompt shown as the placeholder while the field is empty.
44+
* Tab accepts it into the field as editable text — it is never sent on its
45+
* own. Once anything is typed, Tab goes back to being Tab.
46+
*/
47+
placeholderSuggestion?: string;
4148
}) {
4249
const ref = useRef<HTMLTextAreaElement>(null);
4350

@@ -56,7 +63,7 @@ export function DashboardAgentComposer({
5663
// destructive action.
5764
<Button
5865
variant="minimal/small"
59-
className="aspect-square h-6 p-1"
66+
className="aspect-square h-6 min-w-0 p-1"
6067
aria-label="Stop generating"
6168
tooltip="Stop generating"
6269
onClick={onStop}
@@ -65,7 +72,7 @@ export function DashboardAgentComposer({
6572
) : (
6673
<Button
6774
variant="primary/small"
68-
className="aspect-square h-6 p-1"
75+
className="aspect-square h-6 min-w-0 p-1"
6976
aria-label="Send"
7077
tooltip="Send"
7178
onClick={onSubmit}
@@ -106,8 +113,21 @@ export function DashboardAgentComposer({
106113
e.preventDefault();
107114
onSubmit();
108115
}
116+
// Tab accepts the suggested placeholder into the field (still
117+
// editable, not sent). Only while empty — with text present, Tab
118+
// keeps its normal focus behavior.
119+
if (e.key === "Tab" && !e.shiftKey && placeholderSuggestion && value === "") {
120+
e.preventDefault();
121+
onChange(placeholderSuggestion);
122+
requestAnimationFrame(() => {
123+
const el = ref.current;
124+
el?.setSelectionRange(el.value.length, el.value.length);
125+
});
126+
}
109127
}}
110-
placeholder="Type a message…"
128+
placeholder={
129+
placeholderSuggestion ? `${placeholderSuggestion} — Tab to use` : "Type a message…"
130+
}
111131
aria-label="Message the dashboard agent"
112132
className={cn(
113133
"max-h-[40vh] flex-1 resize-none border-0 bg-transparent px-1.5 py-0.5 text-sm leading-6 text-text-bright placeholder-text-dimmed outline-hidden ring-0 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control field-sizing-content focus:outline-hidden focus:ring-0",

apps/webapp/app/components/dashboard-agent/DashboardAgentDraft.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { SuggestedPrompt } from "@internal/dashboard-agent-contracts";
2-
import { useCallback, useState } from "react";
2+
import { useCallback, useMemo, useState } from "react";
33
import { DashboardAgentComposer } from "./DashboardAgentComposer";
44
import { DashboardAgentContextBanner } from "./DashboardAgentContextBanner";
55
import { DashboardAgentHero } from "./DashboardAgentHero";
66
import type { AgentPageContext } from "./page-context-types";
7+
import { readDismissedPromptIds, resolveSuggestedPromptsBySlot } from "./suggested-prompts";
78

89
/**
910
* The new-chat "draft" state: the blank-state hero — title, subtitle, the field
@@ -35,6 +36,22 @@ export function DashboardAgentDraft({
3536
}) {
3637
const [input, setInput] = useState("");
3738

39+
// The top resolved prompt doubles as the field's placeholder (Tab accepts it
40+
// as editable text). Same resolution the hero's buttons use, so the
41+
// placeholder is always the first button the user sees.
42+
const [dismissedIds] = useState(readDismissedPromptIds);
43+
const placeholderSuggestion = useMemo(
44+
() =>
45+
resolveSuggestedPromptsBySlot(
46+
pageContext ?? { page: { kind: "other", path: "" }, signals: [] },
47+
{
48+
promoted: promotedPrompt,
49+
dismissedIds,
50+
}
51+
)[0]?.prompt.prompt,
52+
[pageContext, promotedPrompt, dismissedIds]
53+
);
54+
3855
const submit = useCallback(
3956
(text: string) => {
4057
const trimmed = text.trim();
@@ -58,6 +75,7 @@ export function DashboardAgentDraft({
5875
onSubmit={() => submit(input)}
5976
onStop={() => {}}
6077
isStreaming={false}
78+
placeholderSuggestion={placeholderSuggestion}
6179
context={
6280
<DashboardAgentContextBanner
6381
projectSlug={projectSlug}

0 commit comments

Comments
 (0)