mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-09 16:08:31 +00:00
fix: block unresolved suggestion template placeholders (#3764)
* fix: block unresolved suggestion placeholders * fix: satisfy suggestions config hook deps
This commit is contained in:
parent
73874a9b35
commit
b7d2dcc67c
5 changed files with 127 additions and 18 deletions
|
|
@ -21,7 +21,9 @@ import {
|
|||
useState,
|
||||
type ComponentProps,
|
||||
type KeyboardEvent,
|
||||
type RefObject,
|
||||
} from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
PromptInput,
|
||||
|
|
@ -93,6 +95,20 @@ import { Tooltip } from "./tooltip";
|
|||
type InputMode = "flash" | "thinking" | "pro" | "ultra";
|
||||
|
||||
const MAX_SKILL_SUGGESTIONS = 6;
|
||||
const SUGGESTION_TEMPLATE_PLACEHOLDER_PATTERN =
|
||||
/\[(?:主题|来源|topic|source)\]/i;
|
||||
|
||||
function findSuggestionTemplatePlaceholder(text: string) {
|
||||
const match = SUGGESTION_TEMPLATE_PLACEHOLDER_PATTERN.exec(text);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
start: match.index,
|
||||
end: match.index + match[0].length,
|
||||
};
|
||||
}
|
||||
|
||||
function getLeadingSlashSkillQuery(value: string): string | null {
|
||||
if (!value.startsWith("/")) {
|
||||
|
|
@ -209,6 +225,8 @@ export function InputBox({
|
|||
|
||||
const [followups, setFollowups] = useState<string[]>([]);
|
||||
const { data: suggestionsConfig } = useSuggestionsConfig();
|
||||
const suggestionsConfigLoaded = suggestionsConfig !== undefined;
|
||||
const suggestionsEnabled = suggestionsConfig?.enabled;
|
||||
const [followupsHidden, setFollowupsHidden] = useState(false);
|
||||
const [followupsLoading, setFollowupsLoading] = useState(false);
|
||||
const [textareaFocused, setTextareaFocused] = useState(false);
|
||||
|
|
@ -356,6 +374,21 @@ export function InputBox({
|
|||
if (!message.text.trim() && message.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
const placeholder = findSuggestionTemplatePlaceholder(message.text);
|
||||
if (placeholder) {
|
||||
toast.error(t.inputBox.suggestionPlaceholderRequired);
|
||||
requestAnimationFrame(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(placeholder.start, placeholder.end);
|
||||
});
|
||||
return Promise.reject(
|
||||
new Error("Suggestion template placeholder is unresolved."),
|
||||
);
|
||||
}
|
||||
promptHistoryIndexRef.current = null;
|
||||
promptHistoryDraftRef.current = "";
|
||||
setFollowups([]);
|
||||
|
|
@ -390,6 +423,7 @@ export function InputBox({
|
|||
resolvedModelName,
|
||||
selectedModel?.supports_thinking,
|
||||
status,
|
||||
t.inputBox.suggestionPlaceholderRequired,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
@ -655,7 +689,7 @@ export function InputBox({
|
|||
if (!lastAiId || lastAiId === lastGeneratedForAiIdRef.current) {
|
||||
return;
|
||||
}
|
||||
if (suggestionsConfig === undefined) {
|
||||
if (!suggestionsConfigLoaded) {
|
||||
return;
|
||||
}
|
||||
lastGeneratedForAiIdRef.current = lastAiId;
|
||||
|
|
@ -675,7 +709,7 @@ export function InputBox({
|
|||
return;
|
||||
}
|
||||
|
||||
if (!suggestionsConfig?.enabled) {
|
||||
if (!suggestionsEnabled) {
|
||||
setFollowups([]);
|
||||
return;
|
||||
}
|
||||
|
|
@ -721,8 +755,9 @@ export function InputBox({
|
|||
disabled,
|
||||
isMock,
|
||||
status,
|
||||
suggestionsConfigLoaded,
|
||||
suggestionsEnabled,
|
||||
threadId,
|
||||
suggestionsConfig?.enabled,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
|
@ -1198,7 +1233,7 @@ export function InputBox({
|
|||
searchParams.get("mode") !== "skill" &&
|
||||
!showSkillSuggestions && (
|
||||
<div className="flex items-center justify-center pt-2">
|
||||
<SuggestionList />
|
||||
<SuggestionList textareaRef={textareaRef} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -1227,28 +1262,27 @@ export function InputBox({
|
|||
);
|
||||
}
|
||||
|
||||
function SuggestionList() {
|
||||
function SuggestionList({
|
||||
textareaRef,
|
||||
}: {
|
||||
textareaRef: RefObject<HTMLTextAreaElement | null>;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { textInput } = usePromptInputController();
|
||||
const handleSuggestionClick = useCallback(
|
||||
(prompt: string | undefined) => {
|
||||
if (!prompt) return;
|
||||
textInput.setInput(prompt);
|
||||
setTimeout(() => {
|
||||
const textarea = document.querySelector<HTMLTextAreaElement>(
|
||||
"textarea[name='message']",
|
||||
);
|
||||
if (textarea) {
|
||||
const selStart = prompt.indexOf("[");
|
||||
const selEnd = prompt.indexOf("]");
|
||||
if (selStart !== -1 && selEnd !== -1) {
|
||||
textarea.setSelectionRange(selStart, selEnd + 1);
|
||||
textarea.focus();
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const textarea = textareaRef.current;
|
||||
const placeholder = findSuggestionTemplatePlaceholder(prompt);
|
||||
if (textarea && placeholder) {
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(placeholder.start, placeholder.end);
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
[textInput],
|
||||
[textareaRef, textInput],
|
||||
);
|
||||
return (
|
||||
<Suggestions className="min-h-16 w-full max-w-full justify-center px-4 sm:w-fit sm:px-0">
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ export const enUS: Translations = {
|
|||
"You already have text in the input. Choose how to send it.",
|
||||
followupConfirmAppend: "Append & send",
|
||||
followupConfirmReplace: "Replace & send",
|
||||
suggestionPlaceholderRequired:
|
||||
"Replace the suggestion placeholder before sending.",
|
||||
suggestions: [
|
||||
{
|
||||
suggestion: "Write",
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ export interface Translations {
|
|||
followupConfirmDescription: string;
|
||||
followupConfirmAppend: string;
|
||||
followupConfirmReplace: string;
|
||||
suggestionPlaceholderRequired: string;
|
||||
suggestions: {
|
||||
suggestion: string;
|
||||
prompt: string;
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ export const zhCN: Translations = {
|
|||
followupConfirmDescription: "当前输入框已有内容,选择发送方式。",
|
||||
followupConfirmAppend: "追加并发送",
|
||||
followupConfirmReplace: "替换并发送",
|
||||
suggestionPlaceholderRequired: "发送前请先填写建议模板中的占位内容。",
|
||||
suggestions: [
|
||||
{
|
||||
suggestion: "写作",
|
||||
|
|
|
|||
|
|
@ -143,6 +143,77 @@ test.describe("Chat workspace", () => {
|
|||
});
|
||||
});
|
||||
|
||||
test("blocks suggestion template placeholders until replaced", async ({
|
||||
page,
|
||||
}) => {
|
||||
let streamCalled = false;
|
||||
let submittedText: string | undefined;
|
||||
await page.route("**/runs/stream", (route) => {
|
||||
streamCalled = true;
|
||||
const body = route.request().postDataJSON() as {
|
||||
input?: { messages?: Array<{ content?: unknown }> };
|
||||
};
|
||||
const content = body.input?.messages?.at(-1)?.content;
|
||||
if (typeof content === "string") {
|
||||
submittedText = content;
|
||||
} else if (Array.isArray(content)) {
|
||||
submittedText = content
|
||||
.map((block) =>
|
||||
typeof block === "object" &&
|
||||
block !== null &&
|
||||
"text" in block &&
|
||||
typeof block.text === "string"
|
||||
? block.text
|
||||
: "",
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
return handleRunStream(route);
|
||||
});
|
||||
|
||||
await page.goto("/workspace/chats/new");
|
||||
|
||||
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await page.getByRole("button", { name: /research/i }).click();
|
||||
await expect(textarea).toHaveValue(
|
||||
"Conduct a deep dive research on [topic], and summarize the findings.",
|
||||
);
|
||||
|
||||
await textarea.press("Enter");
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(streamCalled).toBe(false);
|
||||
await expect(textarea).toHaveValue(
|
||||
"Conduct a deep dive research on [topic], and summarize the findings.",
|
||||
);
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
textarea.evaluate((element) => {
|
||||
const input = element as HTMLTextAreaElement;
|
||||
return input.value.slice(input.selectionStart, input.selectionEnd);
|
||||
}),
|
||||
{ timeout: 5_000 },
|
||||
)
|
||||
.toBe("[topic]");
|
||||
|
||||
await textarea.pressSequentially("AI agents");
|
||||
await expect(textarea).toHaveValue(
|
||||
"Conduct a deep dive research on AI agents, and summarize the findings.",
|
||||
);
|
||||
|
||||
await textarea.press("Enter");
|
||||
|
||||
await expect.poll(() => streamCalled, { timeout: 10_000 }).toBeTruthy();
|
||||
await expect
|
||||
.poll(() => submittedText, { timeout: 10_000 })
|
||||
.toBe(
|
||||
"Conduct a deep dive research on AI agents, and summarize the findings.",
|
||||
);
|
||||
});
|
||||
|
||||
test("slash skill command is submitted as normal chat text", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue