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 { InputBox } from "@/components/workspace/input-box";
|
||||
import { MessageList } from "@/components/workspace/messages";
|
||||
import { MessageListSkeleton } from "@/components/workspace/messages/skeleton";
|
||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||
import { ThreadTitle } from "@/components/workspace/thread-title";
|
||||
import { TodoList } from "@/components/workspace/todo-list";
|
||||
|
|
@ -165,10 +164,6 @@ export default function ChatPage() {
|
|||
|
||||
const [hasSubmitted, setHasSubmitted] = useState(false);
|
||||
const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted;
|
||||
const suppressNewThreadSubmitUi =
|
||||
isNewThread && createNewSession && hasSubmitted;
|
||||
const suppressConversationUi =
|
||||
suppressExistingThreadPrefetchUi || suppressNewThreadSubmitUi;
|
||||
|
||||
useEffect(() => {
|
||||
const pageTitle = isNewThread
|
||||
|
|
@ -176,7 +171,7 @@ export default function ChatPage() {
|
|||
: thread.values?.title && thread.values.title !== "Untitled"
|
||||
? thread.values.title
|
||||
: t.pages.untitled;
|
||||
if (thread.isThreadLoading && !suppressConversationUi) {
|
||||
if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) {
|
||||
document.title = `Loading... - ${t.pages.appName}`;
|
||||
} else {
|
||||
document.title = `${pageTitle} - ${t.pages.appName}`;
|
||||
|
|
@ -188,21 +183,19 @@ export default function ChatPage() {
|
|||
t.pages.appName,
|
||||
thread.values.title,
|
||||
thread.isThreadLoading,
|
||||
suppressConversationUi,
|
||||
suppressExistingThreadPrefetchUi,
|
||||
]);
|
||||
|
||||
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
|
||||
useEffect(() => {
|
||||
if (!suppressConversationUi) {
|
||||
setArtifacts(thread.values.artifacts);
|
||||
if (
|
||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
||||
autoSelectFirstArtifact
|
||||
) {
|
||||
if (thread?.values?.artifacts?.length > 0) {
|
||||
setAutoSelectFirstArtifact(false);
|
||||
selectArtifact(thread.values.artifacts[0]!);
|
||||
}
|
||||
setArtifacts(thread.values.artifacts);
|
||||
if (
|
||||
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
|
||||
autoSelectFirstArtifact
|
||||
) {
|
||||
if (thread?.values?.artifacts?.length > 0) {
|
||||
setAutoSelectFirstArtifact(false);
|
||||
selectArtifact(thread.values.artifacts[0]!);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
|
@ -262,7 +255,7 @@ export default function ChatPage() {
|
|||
<div
|
||||
className={cn(
|
||||
"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]">
|
||||
|
|
@ -354,7 +347,7 @@ export default function ChatPage() {
|
|||
<MessageList
|
||||
className={cn(
|
||||
"size-full",
|
||||
(!isNewThread || hasSubmitted) && "pt-10",
|
||||
(!isNewThread || hasSubmitted) && "pt-[20px]",
|
||||
)}
|
||||
threadId={threadId}
|
||||
thread={thread}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,6 @@ export const ArtifactContent = ({
|
|||
...props
|
||||
}: ArtifactContentProps) => (
|
||||
<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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export type ConversationProps = ComponentProps<typeof StickToBottom>;
|
|||
|
||||
export const Conversation = ({ className, ...props }: ConversationProps) => (
|
||||
<StickToBottom
|
||||
className={cn("relative flex-1 overflow-y-hidden", className)}
|
||||
className={cn("relative flex-1 overflow-y-hidden mt-[60px]", className)}
|
||||
initial="smooth"
|
||||
resize="smooth"
|
||||
role="log"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const Message = ({ className, from, ...props }: MessageProps) => (
|
|||
className={cn(
|
||||
"group flex w-full flex-col gap-2 rounded-[10px] p-[20px]",
|
||||
from === "user"
|
||||
? "is-user ml-auto justify-end"
|
||||
? "is-user px-0 ml-auto justify-end"
|
||||
: "is-assistant bg-[#ffffff]",
|
||||
className,
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ interface AuroraTextProps {
|
|||
className?: string;
|
||||
colors?: string[];
|
||||
speed?: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const AuroraText = memo(
|
||||
|
|
@ -15,6 +16,7 @@ export const AuroraText = memo(
|
|||
className = "",
|
||||
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
|
||||
speed = 1,
|
||||
style,
|
||||
}: AuroraTextProps) => {
|
||||
const gradientStyle = {
|
||||
backgroundImage: `linear-gradient(135deg, ${colors.join(", ")}, ${
|
||||
|
|
@ -26,7 +28,7 @@ export const AuroraText = memo(
|
|||
};
|
||||
|
||||
return (
|
||||
<span className={`relative inline-block ${className}`}>
|
||||
<span className={`relative inline-block ${className}`} style={style}>
|
||||
<span className="sr-only">{children}</span>
|
||||
<span
|
||||
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"
|
||||
role="group"
|
||||
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",
|
||||
|
||||
// Variants based on alignment.
|
||||
|
|
|
|||
|
|
@ -305,7 +305,7 @@ export function InputBox({
|
|||
)}
|
||||
inputGroupClassName={cn(
|
||||
"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)]",
|
||||
effectiveIsFocused ? "h-[200px]" : "h-[80px]",
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function MessageList({
|
|||
<Conversation
|
||||
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) => {
|
||||
if (group.type === "human" || group.type === "assistant") {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -41,7 +41,14 @@ export function Welcome({
|
|||
`✨ ${t.welcome.createYourOwnSkill} ✨`
|
||||
) : (
|
||||
<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}
|
||||
</AuroraText>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const zhCN: Translations = {
|
|||
|
||||
// Welcome
|
||||
welcome: {
|
||||
greeting: "使用Skill",
|
||||
greeting: "使用 Skill",
|
||||
description:
|
||||
"欢迎使用 🦌 DeerFlow,一个完全开源的超级智能体。通过内置和自定义的 Skills,\nDeerFlow 可以帮你搜索网络、分析数据,还能为你生成幻灯片、\n图片、视频、播客及网页等,几乎可以做任何事情。",
|
||||
|
||||
|
|
|
|||
|
|
@ -18,29 +18,6 @@ import type {
|
|||
AgentThreadState,
|
||||
} 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({
|
||||
threadId,
|
||||
isNewThread,
|
||||
|
|
@ -212,10 +189,6 @@ export function useSubmitThread({
|
|||
},
|
||||
);
|
||||
|
||||
if (createNewSession && isNewThread && threadId) {
|
||||
await waitForThreadStateToBeReadable(apiClient, threadId);
|
||||
}
|
||||
|
||||
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
|
||||
afterSubmit?.();
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue