feat(frontend): 优化工作区输入框与 artifacts 展示体验

改进工作区核心交互,提升输入与结果查看的一致性和可用性。

调整 prompt 输入相关组件逻辑,优化输入行为与状态反馈
更新 workspace input-box 交互细节,改善可用性与稳定性
优化 message-group 展示逻辑,增强消息区域可读性
调整 artifact-file-detail 预览相关实现,为后续 Office 文件展示做准备
补充并更新 thread-routing e2e 用例,覆盖关键路由与交互回归场景
This commit is contained in:
肖应宇 2026-04-10 17:19:41 +08:00
parent c2ddb1cee5
commit 51e795a289
5 changed files with 43 additions and 12 deletions

View File

@ -883,7 +883,7 @@ export const PromptInputTextarea = ({
if (!submitOnEnter) {
return;
}
if (e.shiftKey) {
if (e.shiftKey || e.ctrlKey || e.metaKey) {
return;
}
e.preventDefault();
@ -1089,10 +1089,12 @@ export const PromptInputSubmit = ({
controller.attachments.files.length > 0
: false;
// 正在 streaming 时不允许发送
const isStreaming = status === "streaming" || status === "submitted";
const isDisabled = disabled || !hasContent || isStreaming;
const isStreaming = status === "streaming";
const isSubmitted = status === "submitted";
// Streaming 时按钮用于停止,不受输入内容是否为空限制
const isDisabled = isStreaming
? !!disabled
: disabled || !hasContent || isSubmitted;
let Icon = <ArrowUpIcon className="size-4" />;

View File

@ -95,8 +95,8 @@ export function ArtifactFileDetail({
return checkCodeFile(filepath);
}, [filepath, isWriteFile, isSkillFile]);
const previewable = useMemo(() => {
return (language === "html" && !isWriteFile) || language === "markdown";
}, [isWriteFile, language]);
return language === "html" || language === "markdown";
}, [language]);
const artifactUrl = useMemo(() => {
if (!threadId) {
return "";

View File

@ -342,7 +342,6 @@ export function InputBox({
"size-full",
!effectiveIsFocused && "h-[80px] py-0 leading-20",
)}
submitOnEnter={false}
disabled={isInputDisabled}
placeholder={t.inputBox.placeholder}
autoFocus={autoFocus}

View File

@ -76,6 +76,9 @@ export function MessageGroup({
return filteredSteps[filteredSteps.length - 1];
}
}, [lastToolCallStep, steps]);
const totalToolStepCount = aboveLastToolCallSteps.length + (lastToolCallStep ? 1 : 0);
const shouldShowToolSteps = !!lastToolCallStep &&
(showAbove || aboveLastToolCallSteps.length === 0);
const rehypePlugins = useRehypeSplitWordsIntoSpans(isLoading);
return (
<ChainOfThought
@ -87,14 +90,17 @@ export function MessageGroup({
key="above"
className="w-full items-start justify-start text-left"
variant="ghost"
onClick={() => setShowAbove(!showAbove)}
onClick={(event) => {
event.stopPropagation();
setShowAbove((prev) => !prev);
}}
>
<ChainOfThoughtStep
label={
<span className="opacity-60">
{showAbove
? t.toolCalls.lessSteps
: t.toolCalls.moreSteps(aboveLastToolCallSteps.length)}
: t.toolCalls.moreSteps(totalToolStepCount)}
</span>
}
icon={
@ -108,7 +114,7 @@ export function MessageGroup({
></ChainOfThoughtStep>
</Button>
)}
{lastToolCallStep && (
{shouldShowToolSteps && (
<ChainOfThoughtContent className="px-4 pb-2">
{showAbove &&
aboveLastToolCallSteps.map((step) =>
@ -145,7 +151,10 @@ export function MessageGroup({
key={lastReasoningStep.id}
className="w-full items-start justify-start text-left"
variant="ghost"
onClick={() => setShowLastThinking(!showLastThinking)}
onClick={(event) => {
event.stopPropagation();
setShowLastThinking((prev) => !prev);
}}
>
<div className="flex w-full items-center justify-between">
<ChainOfThoughtStep

View File

@ -78,4 +78,25 @@ test.describe("线程路由(无 isnew", () => {
{ timeout: 30_000 },
);
});
test("streaming 中点击停止可中断输出", async ({ page }) => {
const threadId = uuid();
const text =
"请逐行输出 1 到 500 的数字并在每一行前面加上“第N行”前缀不要省略。";
await openChat(page, newChatEntry(threadId));
await expect(page.getByTestId("welcome-suggestions")).toBeVisible();
await sendMessage(page, text);
const submitButton = page.locator("button[aria-label='Submit']");
await expect(submitButton).toHaveText("停止", { timeout: 30_000 });
await expect(submitButton).toBeEnabled();
await submitButton.click();
// 点击停止后应退出 streaming 态,按钮文本不再是“停止”
await expect(submitButton).toHaveText("发送", { timeout: 30_000 });
});
});