feat(ui): 重构聊天页布局并规范化 iframe 通信
- 移除 ResizablePanel 组件,改用自定义 flex 布局实现聊天区与 artifacts 面板 - 调整 artifacts 面板样式,支持全屏模式下的布局切换 - 新增 iframe-messages.ts 统一 postMessage 通信协议,定义 FULLSCREEN、SELECT_SKILL 等消息类型 - 优化 artifacts 工具栏图标为 SVG 内联实现,调整 zoom 默认值为 80% - 重构 dropdown-selector 组件,支持展开/收起状态指示器 - 修改 layout.tsx 中 geist variable 的 className 拼接方式 - 新增 package.json prettier 格式化命令
This commit is contained in:
parent
6335424aca
commit
fb226f85a8
|
|
@ -27,7 +27,6 @@ import { DevTodoList } from "@/components/workspace/dev-todo-list";
|
||||||
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
|
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
|
||||||
import { InputBox } from "@/components/workspace/input-box";
|
import { InputBox } from "@/components/workspace/input-box";
|
||||||
import { MessageList } from "@/components/workspace/messages";
|
import { MessageList } from "@/components/workspace/messages";
|
||||||
import { MessageListSkeleton } from "@/components/workspace/messages/skeleton";
|
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||||
import { TodoList } from "@/components/workspace/todo-list";
|
import { TodoList } from "@/components/workspace/todo-list";
|
||||||
|
|
@ -165,10 +164,6 @@ export default function ChatPage() {
|
||||||
|
|
||||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||||
const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted;
|
const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted;
|
||||||
const suppressNewThreadSubmitUi =
|
|
||||||
isNewThread && createNewSession && hasSubmitted;
|
|
||||||
const suppressConversationUi =
|
|
||||||
suppressExistingThreadPrefetchUi || suppressNewThreadSubmitUi;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pageTitle = isNewThread
|
const pageTitle = isNewThread
|
||||||
|
|
@ -176,7 +171,7 @@ export default function ChatPage() {
|
||||||
: thread.values?.title && thread.values.title !== "Untitled"
|
: thread.values?.title && thread.values.title !== "Untitled"
|
||||||
? thread.values.title
|
? thread.values.title
|
||||||
: t.pages.untitled;
|
: t.pages.untitled;
|
||||||
if (thread.isThreadLoading && !suppressConversationUi) {
|
if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) {
|
||||||
document.title = `Loading... - ${t.pages.appName}`;
|
document.title = `Loading... - ${t.pages.appName}`;
|
||||||
} else {
|
} else {
|
||||||
document.title = `${pageTitle} - ${t.pages.appName}`;
|
document.title = `${pageTitle} - ${t.pages.appName}`;
|
||||||
|
|
@ -188,21 +183,19 @@ export default function ChatPage() {
|
||||||
t.pages.appName,
|
t.pages.appName,
|
||||||
thread.values.title,
|
thread.values.title,
|
||||||
thread.isThreadLoading,
|
thread.isThreadLoading,
|
||||||
suppressConversationUi,
|
suppressExistingThreadPrefetchUi,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!suppressConversationUi) {
|
setArtifacts(thread.values.artifacts);
|
||||||
setArtifacts(thread.values.artifacts);
|
if (
|
||||||
if (
|
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
||||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
autoSelectFirstArtifact
|
||||||
autoSelectFirstArtifact
|
) {
|
||||||
) {
|
if (thread?.values?.artifacts?.length > 0) {
|
||||||
if (thread?.values?.artifacts?.length > 0) {
|
setAutoSelectFirstArtifact(false);
|
||||||
setAutoSelectFirstArtifact(false);
|
selectArtifact(thread.values.artifacts[0]!);
|
||||||
selectArtifact(thread.values.artifacts[0]!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|
@ -262,7 +255,7 @@ export default function ChatPage() {
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out",
|
"m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out",
|
||||||
artifactsOpen ? "w-full" : "w-[50%]",
|
artifactsOpen ? "w-full" : "w-[70%]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]">
|
<div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]">
|
||||||
|
|
@ -354,7 +347,7 @@ export default function ChatPage() {
|
||||||
<MessageList
|
<MessageList
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-full",
|
"size-full",
|
||||||
(!isNewThread || hasSubmitted) && "pt-10",
|
(!isNewThread || hasSubmitted) && "pt-[20px]",
|
||||||
)}
|
)}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
thread={thread}
|
thread={thread}
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,6 @@ export const ArtifactContent = ({
|
||||||
...props
|
...props
|
||||||
}: ArtifactContentProps) => (
|
}: ArtifactContentProps) => (
|
||||||
<div className="min-h-0 flex-1 overflow-auto">
|
<div className="min-h-0 flex-1 overflow-auto">
|
||||||
<div className={cn("mb-[208px] p-4", className)} {...props} />
|
<div className={cn("mb-[150px] p-4", className)} {...props} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
||||||
|
|
||||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||||
<StickToBottom
|
<StickToBottom
|
||||||
className={cn("relative flex-1 overflow-y-hidden", className)}
|
className={cn("relative flex-1 overflow-y-hidden mt-[60px]", className)}
|
||||||
initial="smooth"
|
initial="smooth"
|
||||||
resize="smooth"
|
resize="smooth"
|
||||||
role="log"
|
role="log"
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const Message = ({ className, from, ...props }: MessageProps) => (
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex w-full flex-col gap-2 rounded-[10px] p-[20px]",
|
"group flex w-full flex-col gap-2 rounded-[10px] p-[20px]",
|
||||||
from === "user"
|
from === "user"
|
||||||
? "is-user ml-auto justify-end"
|
? "is-user px-0 ml-auto justify-end"
|
||||||
: "is-assistant bg-[#ffffff]",
|
: "is-assistant bg-[#ffffff]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ interface AuroraTextProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
colors?: string[];
|
colors?: string[];
|
||||||
speed?: number;
|
speed?: number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuroraText = memo(
|
export const AuroraText = memo(
|
||||||
|
|
@ -15,6 +16,7 @@ export const AuroraText = memo(
|
||||||
className = "",
|
className = "",
|
||||||
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
||||||
speed = 1,
|
speed = 1,
|
||||||
|
style,
|
||||||
}: AuroraTextProps) => {
|
}: AuroraTextProps) => {
|
||||||
const gradientStyle = {
|
const gradientStyle = {
|
||||||
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
||||||
|
|
@ -26,7 +28,7 @@ export const AuroraText = memo(
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={`relative inline-block ${className}`}>
|
<span className={`relative inline-block ${className}`} style={style}>
|
||||||
<span className="sr-only">{children}</span>
|
<span className="sr-only">{children}</span>
|
||||||
<span
|
<span
|
||||||
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
|
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/input-group dark:bg-background/80 relative flex w-full max-w-[720px] items-center overflow-hidden rounded-md bg-[#FBFAFC] shadow-[0_0_20px_0_rgba(0,0,0,0.10)] transition-[color,box-shadow] outline-none",
|
"group/input-group dark:bg-background/80 relative flex w-full max-w-[720px] items-center overflow-hidden rounded-md bg-[#FBFAFC] transition-[color,box-shadow] outline-none",
|
||||||
"h-9 min-w-0 has-[>textarea]:h-auto",
|
"h-9 min-w-0 has-[>textarea]:h-auto",
|
||||||
|
|
||||||
// Variants based on alignment.
|
// Variants based on alignment.
|
||||||
|
|
|
||||||
|
|
@ -305,7 +305,7 @@ export function InputBox({
|
||||||
)}
|
)}
|
||||||
inputGroupClassName={cn(
|
inputGroupClassName={cn(
|
||||||
"border-0 rounded-[20px] backdrop-blur-sm",
|
"border-0 rounded-[20px] backdrop-blur-sm",
|
||||||
"transition-[height] duration-300 ease-out",
|
"transition-[height] duration-300 ease-out shadow-none ",
|
||||||
!isNewThread && "h-[200px] shadow-[0_0_20px_0_rgba(0,0,0,0.10)]",
|
!isNewThread && "h-[200px] shadow-[0_0_20px_0_rgba(0,0,0,0.10)]",
|
||||||
effectiveIsFocused ? "h-[200px]" : "h-[80px]",
|
effectiveIsFocused ? "h-[200px]" : "h-[80px]",
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export function MessageList({
|
||||||
<Conversation
|
<Conversation
|
||||||
className={cn("flex size-full flex-col justify-center", className)}
|
className={cn("flex size-full flex-col justify-center", className)}
|
||||||
>
|
>
|
||||||
<ConversationContent className="w-full gap-8 px-[20px] pt-12">
|
<ConversationContent className="w-full gap-8 px-[20px]">
|
||||||
{groupMessages(messages, (group) => {
|
{groupMessages(messages, (group) => {
|
||||||
if (group.type === "human" || group.type === "assistant") {
|
if (group.type === "human" || group.type === "assistant") {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,14 @@ export function Welcome({
|
||||||
`✨ ${t.welcome.createYourOwnSkill} ✨`
|
`✨ ${t.welcome.createYourOwnSkill} ✨`
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<AuroraText className="text-[18px] text-[#150033]" colors={colors}>
|
<AuroraText
|
||||||
|
className="text-center font-normal text-[18px] leading-normal"
|
||||||
|
style={{
|
||||||
|
color: "var(--color-150033, #150033)",
|
||||||
|
fontFamily: '"Microsoft YaHei"',
|
||||||
|
}}
|
||||||
|
colors={colors}
|
||||||
|
>
|
||||||
{t.welcome.greeting}
|
{t.welcome.greeting}
|
||||||
</AuroraText>
|
</AuroraText>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export const zhCN: Translations = {
|
||||||
|
|
||||||
// Welcome
|
// Welcome
|
||||||
welcome: {
|
welcome: {
|
||||||
greeting: "使用Skill",
|
greeting: "使用 Skill",
|
||||||
description:
|
description:
|
||||||
"欢迎使用 🦌 DeerFlow,一个完全开源的超级智能体。通过内置和自定义的 Skills,\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等,几乎可以做任何事情。",
|
"欢迎使用 🦌 DeerFlow,一个完全开源的超级智能体。通过内置和自定义的 Skills,\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等,几乎可以做任何事情。",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,29 +18,6 @@ import type {
|
||||||
AgentThreadState,
|
AgentThreadState,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
|
|
||||||
async function waitForThreadStateToBeReadable(
|
|
||||||
apiClient: ReturnType<typeof getAPIClient>,
|
|
||||||
threadId: string,
|
|
||||||
timeoutMs = 3000,
|
|
||||||
) {
|
|
||||||
const deadline = Date.now() + timeoutMs;
|
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
try {
|
|
||||||
const state = await apiClient.threads.getState<AgentThreadState>(threadId);
|
|
||||||
if ((state.values.messages?.length ?? 0) > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore transient 404 / not-ready errors while the new thread is being persisted.
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useThreadStream({
|
export function useThreadStream({
|
||||||
threadId,
|
threadId,
|
||||||
isNewThread,
|
isNewThread,
|
||||||
|
|
@ -212,10 +189,6 @@ export function useSubmitThread({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (createNewSession && isNewThread && threadId) {
|
|
||||||
await waitForThreadStateToBeReadable(apiClient, threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||||
afterSubmit?.();
|
afterSubmit?.();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue