mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-08-01 19:06:01 +00:00
fix(frontend): improve mobile workspace layout (#3646)
This commit is contained in:
parent
6044e5c553
commit
86a4744941
@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from "react";
|
|||||||
|
|
||||||
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { AgentWelcome } from "@/components/workspace/agent-welcome";
|
import { AgentWelcome } from "@/components/workspace/agent-welcome";
|
||||||
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
||||||
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
|
import { ChatBox, useThreadChat } from "@/components/workspace/chats";
|
||||||
@ -168,33 +169,36 @@ export default function AgentChatPage() {
|
|||||||
<div className="relative flex size-full min-h-0 justify-between">
|
<div className="relative flex size-full min-h-0 justify-between">
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute top-0 right-0 left-0 z-30 flex h-12 shrink-0 items-center gap-2 px-4",
|
"absolute top-0 right-0 left-0 z-30 flex h-12 shrink-0 items-center gap-2 px-2 sm:px-4",
|
||||||
isWelcomeMode
|
isWelcomeMode
|
||||||
? "bg-background/0 backdrop-blur-none"
|
? "bg-background/0 backdrop-blur-none"
|
||||||
: "bg-background/80 shadow-xs backdrop-blur",
|
: "bg-background/80 shadow-xs backdrop-blur",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<SidebarTrigger className="md:hidden" />
|
||||||
{/* Agent badge */}
|
{/* Agent badge */}
|
||||||
<div className="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1">
|
<div className="flex min-w-0 shrink-0 items-center gap-1.5 rounded-md border px-2 py-1">
|
||||||
<BotIcon className="text-primary h-3.5 w-3.5" />
|
<BotIcon className="text-primary h-3.5 w-3.5" />
|
||||||
<span className="text-xs font-medium">
|
<span className="hidden max-w-24 truncate text-xs font-medium sm:inline sm:max-w-none">
|
||||||
{agent?.name ?? agent_name}
|
{agent?.name ?? agent_name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full items-center text-sm font-medium">
|
<div className="flex min-w-0 flex-1 items-center text-sm font-medium">
|
||||||
<ThreadTitle threadId={threadId} thread={thread} />
|
<ThreadTitle threadId={threadId} thread={thread} />
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-4 flex items-center">
|
<div className="flex shrink-0 items-center sm:mr-4">
|
||||||
<Tooltip content={t.agents.newChat}>
|
<Tooltip content={t.agents.newChat}>
|
||||||
<Button
|
<Button
|
||||||
|
className="px-2 sm:px-3"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push(`/workspace/agents/${agent_name}/chats/new`);
|
router.push(`/workspace/agents/${agent_name}/chats/new`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusSquare /> {t.agents.newChat}
|
<PlusSquare />
|
||||||
|
<span className="hidden sm:inline">{t.agents.newChat}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<TokenUsageIndicator
|
<TokenUsageIndicator
|
||||||
@ -229,14 +233,15 @@ export default function AgentChatPage() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"right-0 bottom-0 left-0 z-30 flex justify-center px-4",
|
"right-0 bottom-0 left-0 z-30 flex justify-center px-3 sm:px-4",
|
||||||
isWelcomeMode ? "absolute" : "relative shrink-0 pb-4",
|
isWelcomeMode ? "absolute" : "relative shrink-0 pb-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-full",
|
"relative w-full",
|
||||||
isWelcomeMode && "-translate-y-[calc(50vh-96px)]",
|
isWelcomeMode &&
|
||||||
|
"-translate-y-[calc(50vh-48px)] sm:-translate-y-[calc(50vh-96px)]",
|
||||||
isWelcomeMode
|
isWelcomeMode
|
||||||
? "max-w-(--container-width-sm)"
|
? "max-w-(--container-width-sm)"
|
||||||
: "max-w-(--container-width-md)",
|
: "max-w-(--container-width-md)",
|
||||||
@ -267,7 +272,7 @@ export default function AgentChatPage() {
|
|||||||
<InputBox
|
<InputBox
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background/5 w-full",
|
"bg-background/5 w-full",
|
||||||
isWelcomeMode && "-translate-y-4",
|
isWelcomeMode && "-translate-y-2 sm:-translate-y-4",
|
||||||
)}
|
)}
|
||||||
isWelcomeMode={isWelcomeMode}
|
isWelcomeMode={isWelcomeMode}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
import { type PromptInputMessage } from "@/components/ai-elements/prompt-input";
|
||||||
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
import { ArtifactTrigger } from "@/components/workspace/artifacts";
|
||||||
import {
|
import {
|
||||||
ChatBox,
|
ChatBox,
|
||||||
@ -170,16 +171,17 @@ export default function ChatPage() {
|
|||||||
<div className="relative flex size-full min-h-0 justify-between">
|
<div className="relative flex size-full min-h-0 justify-between">
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute top-0 right-0 left-0 z-30 flex h-12 shrink-0 items-center px-4",
|
"absolute top-0 right-0 left-0 z-30 flex h-12 shrink-0 items-center gap-2 px-2 sm:px-4",
|
||||||
isWelcomeMode
|
isWelcomeMode
|
||||||
? "bg-background/0 backdrop-blur-none"
|
? "bg-background/0 backdrop-blur-none"
|
||||||
: "bg-background/80 shadow-xs backdrop-blur",
|
: "bg-background/80 shadow-xs backdrop-blur",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex w-full items-center text-sm font-medium">
|
<SidebarTrigger className="md:hidden" />
|
||||||
|
<div className="flex min-w-0 flex-1 items-center text-sm font-medium">
|
||||||
<ThreadTitle threadId={threadId} thread={thread} />
|
<ThreadTitle threadId={threadId} thread={thread} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
<TokenUsageIndicator
|
<TokenUsageIndicator
|
||||||
threadId={isNewThread ? undefined : threadId}
|
threadId={isNewThread ? undefined : threadId}
|
||||||
backendUsage={backendTokenUsage}
|
backendUsage={backendTokenUsage}
|
||||||
@ -210,14 +212,15 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"right-0 bottom-0 left-0 z-30 flex justify-center px-4",
|
"right-0 bottom-0 left-0 z-30 flex justify-center px-3 sm:px-4",
|
||||||
isWelcomeMode ? "absolute" : "relative shrink-0 pb-4",
|
isWelcomeMode ? "absolute" : "relative shrink-0 pb-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-full",
|
"relative w-full",
|
||||||
isWelcomeMode && "-translate-y-[calc(50vh-96px)]",
|
isWelcomeMode &&
|
||||||
|
"-translate-y-[calc(50vh-48px)] sm:-translate-y-[calc(50vh-96px)]",
|
||||||
isWelcomeMode
|
isWelcomeMode
|
||||||
? "max-w-(--container-width-sm)"
|
? "max-w-(--container-width-sm)"
|
||||||
: "max-w-(--container-width-md)",
|
: "max-w-(--container-width-md)",
|
||||||
@ -248,7 +251,7 @@ export default function ChatPage() {
|
|||||||
<InputBox
|
<InputBox
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background/5 w-full",
|
"bg-background/5 w-full",
|
||||||
isWelcomeMode && "-translate-y-4",
|
isWelcomeMode && "-translate-y-2 sm:-translate-y-4",
|
||||||
)}
|
)}
|
||||||
isWelcomeMode={isWelcomeMode}
|
isWelcomeMode={isWelcomeMode}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
@ -280,7 +283,7 @@ export default function ChatPage() {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background/5 h-32 w-full rounded-2xl",
|
"bg-background/5 h-32 w-full rounded-2xl",
|
||||||
isWelcomeMode && "-translate-y-4",
|
isWelcomeMode && "-translate-y-2 sm:-translate-y-4",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -582,7 +582,7 @@ export function InputBox({
|
|||||||
<div
|
<div
|
||||||
ref={promptRootRef}
|
ref={promptRootRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex flex-col",
|
"relative flex min-w-0 flex-col",
|
||||||
isWelcomeMode ? "gap-4" : "gap-2",
|
isWelcomeMode ? "gap-4" : "gap-2",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -694,8 +694,8 @@ export function InputBox({
|
|||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
/>
|
/>
|
||||||
</PromptInputBody>
|
</PromptInputBody>
|
||||||
<PromptInputFooter className="flex">
|
<PromptInputFooter className="flex flex-wrap gap-2 sm:flex-nowrap">
|
||||||
<PromptInputTools>
|
<PromptInputTools className="min-w-0 flex-1 flex-wrap">
|
||||||
{/* TODO: Add more connectors here
|
{/* TODO: Add more connectors here
|
||||||
<PromptInputActionMenu>
|
<PromptInputActionMenu>
|
||||||
<PromptInputActionMenuTrigger className="px-2!" />
|
<PromptInputActionMenuTrigger className="px-2!" />
|
||||||
@ -717,7 +717,7 @@ export function InputBox({
|
|||||||
: "flash"
|
: "flash"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PromptInputActionMenuTrigger className="gap-1! px-2!">
|
<PromptInputActionMenuTrigger className="max-w-28 gap-1! px-2! sm:max-w-none">
|
||||||
<div>
|
<div>
|
||||||
{context.mode === "flash" && <ZapIcon className="size-3" />}
|
{context.mode === "flash" && <ZapIcon className="size-3" />}
|
||||||
{context.mode === "thinking" && (
|
{context.mode === "thinking" && (
|
||||||
@ -732,7 +732,7 @@ export function InputBox({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-xs font-normal",
|
"truncate text-xs font-normal",
|
||||||
context.mode === "ultra" ? "golden-text" : "",
|
context.mode === "ultra" ? "golden-text" : "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -879,7 +879,7 @@ export function InputBox({
|
|||||||
</PromptInputActionMenu>
|
</PromptInputActionMenu>
|
||||||
{supportReasoningEffort && context.mode !== "flash" && (
|
{supportReasoningEffort && context.mode !== "flash" && (
|
||||||
<PromptInputActionMenu>
|
<PromptInputActionMenu>
|
||||||
<PromptInputActionMenuTrigger className="gap-1! px-2!">
|
<PromptInputActionMenuTrigger className="hidden gap-1! px-2! sm:inline-flex">
|
||||||
<div className="text-xs font-normal">
|
<div className="text-xs font-normal">
|
||||||
{t.inputBox.reasoningEffort}:
|
{t.inputBox.reasoningEffort}:
|
||||||
{context.reasoning_effort === "minimal" &&
|
{context.reasoning_effort === "minimal" &&
|
||||||
@ -994,13 +994,13 @@ export function InputBox({
|
|||||||
</PromptInputActionMenu>
|
</PromptInputActionMenu>
|
||||||
)}
|
)}
|
||||||
</PromptInputTools>
|
</PromptInputTools>
|
||||||
<PromptInputTools>
|
<PromptInputTools className="min-w-0 justify-end">
|
||||||
<ModelSelector
|
<ModelSelector
|
||||||
open={modelDialogOpen}
|
open={modelDialogOpen}
|
||||||
onOpenChange={setModelDialogOpen}
|
onOpenChange={setModelDialogOpen}
|
||||||
>
|
>
|
||||||
<ModelSelectorTrigger asChild>
|
<ModelSelectorTrigger asChild>
|
||||||
<PromptInputButton>
|
<PromptInputButton className="max-w-40 min-w-0 sm:max-w-56">
|
||||||
<div className="flex min-w-0 flex-col items-start text-left">
|
<div className="flex min-w-0 flex-col items-start text-left">
|
||||||
<ModelSelectorName className="text-xs font-normal">
|
<ModelSelectorName className="text-xs font-normal">
|
||||||
{selectedModel?.display_name}
|
{selectedModel?.display_name}
|
||||||
@ -1103,7 +1103,7 @@ function SuggestionList() {
|
|||||||
[textInput],
|
[textInput],
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Suggestions className="min-h-16 w-fit items-start">
|
<Suggestions className="min-h-16 w-full max-w-full justify-center px-4 sm:w-fit sm:px-0">
|
||||||
<ConfettiButton
|
<ConfettiButton
|
||||||
className="text-muted-foreground cursor-pointer rounded-full px-4 text-xs font-normal"
|
className="text-muted-foreground cursor-pointer rounded-full px-4 text-xs font-normal"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@ -10,6 +10,14 @@ import { AuroraText } from "../ui/aurora-text";
|
|||||||
|
|
||||||
let waved = false;
|
let waved = false;
|
||||||
|
|
||||||
|
function WelcomeDescription({ children }: { children: string }) {
|
||||||
|
return (
|
||||||
|
<p className="max-w-full text-wrap break-words whitespace-pre-line">
|
||||||
|
{children}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Welcome({
|
export function Welcome({
|
||||||
className,
|
className,
|
||||||
mode,
|
mode,
|
||||||
@ -32,15 +40,15 @@ export function Welcome({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mx-auto flex w-full flex-col items-center justify-center gap-2 px-8 py-4 text-center",
|
"mx-auto flex w-full max-w-full flex-col items-center justify-center gap-2 px-4 py-4 text-center sm:px-8",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="text-2xl font-bold">
|
<div className="max-w-full text-2xl font-bold">
|
||||||
{searchParams.get("mode") === "skill" ? (
|
{searchParams.get("mode") === "skill" ? (
|
||||||
`✨ ${t.welcome.createYourOwnSkill} ✨`
|
`✨ ${t.welcome.createYourOwnSkill} ✨`
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex max-w-full flex-wrap items-center justify-center gap-2">
|
||||||
<div className={cn("inline-block", !waved ? "animate-wave" : "")}>
|
<div className={cn("inline-block", !waved ? "animate-wave" : "")}>
|
||||||
{isUltra ? "🚀" : "👋"}
|
{isUltra ? "🚀" : "👋"}
|
||||||
</div>
|
</div>
|
||||||
@ -49,24 +57,14 @@ export function Welcome({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{searchParams.get("mode") === "skill" ? (
|
{searchParams.get("mode") === "skill" ? (
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground max-w-full text-sm">
|
||||||
{t.welcome.createYourOwnSkillDescription.includes("\n") ? (
|
<WelcomeDescription>
|
||||||
<pre className="font-sans whitespace-pre">
|
{t.welcome.createYourOwnSkillDescription}
|
||||||
{t.welcome.createYourOwnSkillDescription}
|
</WelcomeDescription>
|
||||||
</pre>
|
|
||||||
) : (
|
|
||||||
<p>{t.welcome.createYourOwnSkillDescription}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground max-w-full text-sm">
|
||||||
{t.welcome.description.includes("\n") ? (
|
<WelcomeDescription>{t.welcome.description}</WelcomeDescription>
|
||||||
<pre className="font-sans whitespace-pre">
|
|
||||||
{t.welcome.description}
|
|
||||||
</pre>
|
|
||||||
) : (
|
|
||||||
<p>{t.welcome.description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
BreadcrumbPage,
|
BreadcrumbPage,
|
||||||
BreadcrumbSeparator,
|
BreadcrumbSeparator,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@ -51,7 +52,8 @@ export function WorkspaceHeader({
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 px-4">
|
<div className="flex min-w-0 items-center gap-2 px-2 sm:px-4">
|
||||||
|
<SidebarTrigger className="md:hidden" />
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
{segments?.[0] && (
|
{segments?.[0] && (
|
||||||
|
|||||||
@ -29,4 +29,45 @@ test.describe("Sidebar navigation", () => {
|
|||||||
await page.waitForURL("**/workspace/agents");
|
await page.waitForURL("**/workspace/agents");
|
||||||
await expect(page).toHaveURL(/\/workspace\/agents/);
|
await expect(page).toHaveURL(/\/workspace\/agents/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("mobile welcome layout stays within viewport and opens sidebar", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
mockLangGraphAPI(page);
|
||||||
|
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
|
||||||
|
const viewportWidth = page.viewportSize()?.width ?? 390;
|
||||||
|
const expectInsideViewport = async (
|
||||||
|
locator: ReturnType<typeof page.locator>,
|
||||||
|
) => {
|
||||||
|
await expect(locator).toBeVisible({ timeout: 15_000 });
|
||||||
|
const box = await locator.boundingBox();
|
||||||
|
expect(box).not.toBeNull();
|
||||||
|
expect(box!.x).toBeGreaterThanOrEqual(-1);
|
||||||
|
expect(box!.x + box!.width).toBeLessThanOrEqual(viewportWidth + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
await expectInsideViewport(page.getByText(/Welcome to|欢迎使用/).first());
|
||||||
|
await expectInsideViewport(page.getByRole("textbox").first());
|
||||||
|
await expectInsideViewport(page.locator("[data-slot='suggestions-list']"));
|
||||||
|
|
||||||
|
const mobileSidebarTrigger = page
|
||||||
|
.locator("[data-sidebar='trigger']:visible")
|
||||||
|
.first();
|
||||||
|
await expect(mobileSidebarTrigger).toBeVisible();
|
||||||
|
await mobileSidebarTrigger.click();
|
||||||
|
|
||||||
|
const mobileSidebar = page.locator(
|
||||||
|
"[data-mobile='true'][data-sidebar='sidebar']",
|
||||||
|
);
|
||||||
|
await expect(mobileSidebar).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
mobileSidebar.locator("a[href='/workspace/chats']"),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
mobileSidebar.locator("a[href='/workspace/agents']"),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user