mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-09 16:08:31 +00:00
feat: add prompt history recall (#3718)
This commit is contained in:
parent
b990da785f
commit
14d9bb87a4
3 changed files with 232 additions and 1 deletions
|
|
@ -66,6 +66,7 @@ import { useSkills } from "@/core/skills/hooks";
|
|||
import { useSuggestionsConfig } from "@/core/suggestions/hooks";
|
||||
import type { AgentThreadContext } from "@/core/threads";
|
||||
import { textOfMessage } from "@/core/threads/utils";
|
||||
import { isIMEComposing } from "@/lib/ime";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import {
|
||||
|
|
@ -203,6 +204,8 @@ export function InputBox({
|
|||
const { skills } = useSkills();
|
||||
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const promptHistoryIndexRef = useRef<number | null>(null);
|
||||
const promptHistoryDraftRef = useRef("");
|
||||
|
||||
const [followups, setFollowups] = useState<string[]>([]);
|
||||
const { data: suggestionsConfig } = useSuggestionsConfig();
|
||||
|
|
@ -261,6 +264,44 @@ export function InputBox({
|
|||
[selectedModel],
|
||||
);
|
||||
|
||||
const promptHistory = useMemo(() => {
|
||||
const history: string[] = [];
|
||||
for (const message of thread.messages) {
|
||||
if (message.type !== "human") {
|
||||
continue;
|
||||
}
|
||||
const additionalKwargs = message.additional_kwargs;
|
||||
if (
|
||||
additionalKwargs &&
|
||||
typeof additionalKwargs === "object" &&
|
||||
Reflect.get(additionalKwargs, "hide_from_ui") === true
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const text = textOfMessage(message)?.trim();
|
||||
if (!text) {
|
||||
continue;
|
||||
}
|
||||
if (history.at(-1) !== text) {
|
||||
history.push(text);
|
||||
}
|
||||
}
|
||||
return history;
|
||||
}, [thread.messages]);
|
||||
|
||||
useEffect(() => {
|
||||
promptHistoryIndexRef.current = null;
|
||||
promptHistoryDraftRef.current = "";
|
||||
}, [threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentIndex = promptHistoryIndexRef.current;
|
||||
if (currentIndex !== null && currentIndex >= promptHistory.length) {
|
||||
promptHistoryIndexRef.current = null;
|
||||
promptHistoryDraftRef.current = "";
|
||||
}
|
||||
}, [promptHistory.length]);
|
||||
|
||||
const handleModelSelect = useCallback(
|
||||
(model_name: string) => {
|
||||
const model = models.find((m) => m.name === model_name);
|
||||
|
|
@ -315,6 +356,8 @@ export function InputBox({
|
|||
if (!message.text.trim() && message.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
promptHistoryIndexRef.current = null;
|
||||
promptHistoryDraftRef.current = "";
|
||||
setFollowups([]);
|
||||
setFollowupsHidden(false);
|
||||
setFollowupsLoading(false);
|
||||
|
|
@ -489,6 +532,91 @@ export function InputBox({
|
|||
],
|
||||
);
|
||||
|
||||
const setPromptHistoryValue = useCallback(
|
||||
(value: string) => {
|
||||
textInput.setInput(value);
|
||||
requestAnimationFrame(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(value.length, value.length);
|
||||
});
|
||||
},
|
||||
[textInput],
|
||||
);
|
||||
|
||||
const handlePromptHistoryKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (
|
||||
event.altKey ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
event.shiftKey ||
|
||||
isIMEComposing(event) ||
|
||||
promptHistory.length === 0 ||
|
||||
(event.key !== "ArrowUp" && event.key !== "ArrowDown")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue = textInput.value ?? "";
|
||||
const currentHistoryIndex = promptHistoryIndexRef.current;
|
||||
const isBrowsingHistory = currentHistoryIndex !== null;
|
||||
|
||||
if (!isBrowsingHistory && currentValue.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
const nextIndex = isBrowsingHistory
|
||||
? Math.max(currentHistoryIndex - 1, 0)
|
||||
: promptHistory.length - 1;
|
||||
if (!isBrowsingHistory) {
|
||||
promptHistoryDraftRef.current = currentValue;
|
||||
}
|
||||
promptHistoryIndexRef.current = nextIndex;
|
||||
setPromptHistoryValue(promptHistory[nextIndex] ?? "");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isBrowsingHistory) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
if (currentHistoryIndex >= promptHistory.length - 1) {
|
||||
promptHistoryIndexRef.current = null;
|
||||
setPromptHistoryValue(promptHistoryDraftRef.current);
|
||||
promptHistoryDraftRef.current = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = currentHistoryIndex + 1;
|
||||
promptHistoryIndexRef.current = nextIndex;
|
||||
setPromptHistoryValue(promptHistory[nextIndex] ?? "");
|
||||
},
|
||||
[promptHistory, setPromptHistoryValue, textInput.value],
|
||||
);
|
||||
|
||||
const handlePromptTextareaKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
handleSkillSuggestionKeyDown(event);
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
handlePromptHistoryKeyDown(event);
|
||||
},
|
||||
[handlePromptHistoryKeyDown, handleSkillSuggestionKeyDown],
|
||||
);
|
||||
|
||||
const handlePromptTextareaChange = useCallback(() => {
|
||||
promptHistoryIndexRef.current = null;
|
||||
promptHistoryDraftRef.current = "";
|
||||
}, []);
|
||||
|
||||
const showFollowups =
|
||||
!disabled &&
|
||||
!isWelcomeMode &&
|
||||
|
|
@ -708,8 +836,9 @@ export function InputBox({
|
|||
autoFocus={autoFocus}
|
||||
defaultValue={initialValue}
|
||||
onBlur={() => setTextareaFocused(false)}
|
||||
onChange={handlePromptTextareaChange}
|
||||
onFocus={() => setTextareaFocused(true)}
|
||||
onKeyDown={handleSkillSuggestionKeyDown}
|
||||
onKeyDown={handlePromptTextareaKeyDown}
|
||||
ref={textareaRef}
|
||||
/>
|
||||
</PromptInputBody>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,44 @@ test.describe("Chat workspace", () => {
|
|||
await expect(textarea).toHaveValue("/data-analysis ");
|
||||
});
|
||||
|
||||
test("uses arrow keys to navigate skill suggestions before prompt history", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/workspace/chats/new");
|
||||
|
||||
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await textarea.fill("/");
|
||||
|
||||
const dataAnalysis = page.getByRole("option", {
|
||||
name: /data-analysis/i,
|
||||
});
|
||||
const frontendDesign = page.getByRole("option", {
|
||||
name: /frontend-design/i,
|
||||
});
|
||||
await expect(dataAnalysis).toBeVisible();
|
||||
await expect(frontendDesign).toBeVisible();
|
||||
await expect(dataAnalysis).toHaveAttribute("aria-selected", "true");
|
||||
|
||||
await textarea.press("ArrowDown");
|
||||
|
||||
await expect(textarea).toHaveValue("/");
|
||||
await expect(dataAnalysis).toHaveAttribute("aria-selected", "false");
|
||||
await expect(frontendDesign).toHaveAttribute("aria-selected", "true");
|
||||
|
||||
await textarea.press("ArrowUp");
|
||||
|
||||
await expect(textarea).toHaveValue("/");
|
||||
await expect(dataAnalysis).toHaveAttribute("aria-selected", "true");
|
||||
await expect(frontendDesign).toHaveAttribute("aria-selected", "false");
|
||||
|
||||
await textarea.press("ArrowDown");
|
||||
await textarea.press("Enter");
|
||||
|
||||
await expect(textarea).toHaveValue("/frontend-design ");
|
||||
});
|
||||
|
||||
test("keeps Shift+Enter as newline while skill suggestions are visible", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,70 @@ test.describe("Thread history", () => {
|
|||
).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test("input box recalls previous prompts with arrow keys", async ({
|
||||
page,
|
||||
}) => {
|
||||
const firstPrompt = "Summarize the latest quarterly report";
|
||||
const secondPrompt = "Turn the summary into an action plan";
|
||||
|
||||
mockLangGraphAPI(page, {
|
||||
threads: [
|
||||
{
|
||||
thread_id: MOCK_THREAD_ID,
|
||||
title: "Prompt history conversation",
|
||||
updated_at: "2025-06-03T12:00:00Z",
|
||||
messages: [
|
||||
{
|
||||
type: "human",
|
||||
id: "msg-human-prompt-history-1",
|
||||
content: [{ type: "text", text: firstPrompt }],
|
||||
},
|
||||
{
|
||||
type: "ai",
|
||||
id: "msg-ai-prompt-history-1",
|
||||
content: "First answer",
|
||||
},
|
||||
{
|
||||
type: "human",
|
||||
id: "msg-human-prompt-history-2",
|
||||
content: [{ type: "text", text: secondPrompt }],
|
||||
},
|
||||
{
|
||||
type: "ai",
|
||||
id: "msg-ai-prompt-history-2",
|
||||
content: "Second answer",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
|
||||
await expect(page.getByText("Second answer")).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
const textarea = page.locator("textarea[name='message']");
|
||||
await expect(textarea).toBeVisible();
|
||||
|
||||
await textarea.focus();
|
||||
await textarea.press("ArrowUp");
|
||||
await expect(textarea).toHaveValue(secondPrompt);
|
||||
|
||||
await textarea.press("ArrowUp");
|
||||
await expect(textarea).toHaveValue(firstPrompt);
|
||||
|
||||
await textarea.press("ArrowDown");
|
||||
await expect(textarea).toHaveValue(secondPrompt);
|
||||
|
||||
await textarea.press("ArrowDown");
|
||||
await expect(textarea).toHaveValue("");
|
||||
|
||||
await textarea.fill("draft should not be overwritten");
|
||||
await textarea.press("ArrowUp");
|
||||
await expect(textarea).toHaveValue("draft should not be overwritten");
|
||||
});
|
||||
|
||||
test("deleting an inactive chat keeps the current chat open", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue