From cb0ebf41bb3f1c7538c080f2acd051df612f43f8 Mon Sep 17 00:00:00 2001 From: MT-Mint <798521692@qq.com> Date: Thu, 19 Mar 2026 17:32:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E9=87=8D=E6=9E=84=E8=81=8A?= =?UTF-8?q?=E5=A4=A9=E9=A1=B5=E5=B8=83=E5=B1=80=E5=B9=B6=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E5=8C=96iframe=20=E9=80=9A=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package.json | 2 + frontend/src/app/layout.tsx | 2 +- .../workspace/chats/[thread_id]/layout.tsx | 2 +- .../app/workspace/chats/[thread_id]/page.tsx | 60 +++++---- frontend/src/app/workspace/layout.tsx | 2 +- .../src/components/ai-elements/artifact.tsx | 4 +- .../src/components/ui/dropdown-selector.tsx | 50 +++++++- frontend/src/components/ui/toggle-group.tsx | 4 +- .../artifacts/artifact-file-detail.tsx | 114 +++++++++--------- .../workspace/messages/message-list.tsx | 2 +- frontend/src/core/iframe-messages.ts | 55 +++++++++ frontend/src/hooks/use-iframe-skill.ts | 29 ++--- 12 files changed, 212 insertions(+), 114 deletions(-) create mode 100644 frontend/src/core/iframe-messages.ts diff --git a/frontend/package.json b/frontend/package.json index 46ca46a1..f12500fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,6 +8,8 @@ "build": "next build", "check": "next lint && tsc --noEmit", "dev": "next dev --turbo", + "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css,md}\"", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "eslint . --ext .ts,.tsx --fix", "preview": "next build && next start", diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 60f25818..d861be04 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -25,7 +25,7 @@ export default async function RootLayout({ return ( diff --git a/frontend/src/app/workspace/chats/[thread_id]/layout.tsx b/frontend/src/app/workspace/chats/[thread_id]/layout.tsx index 87710377..3d540ccd 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/layout.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/layout.tsx @@ -16,4 +16,4 @@ export default function ChatLayout({ ); -} +} \ No newline at end of file diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 3654a6fa..dc887b11 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -17,11 +17,6 @@ import { DevDialogHeader, DevDialogTitle, } from "@/components/ui/dev-dialog"; -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@/components/ui/resizable"; import { useSidebar } from "@/components/ui/sidebar"; import { ArtifactFileDetail, @@ -257,13 +252,19 @@ export default function ChatPage() { return ( - - -
+
+
+
+
-
+
{title !== "Untitled" && ( )} @@ -336,7 +337,7 @@ export default function ChatPage() {
@@ -358,21 +359,13 @@ export default function ChatPage() {
- - +
-
) : (
-
+
+
{/* Fixed 底部居中输入框容器 */}
*/} +
); } diff --git a/frontend/src/app/workspace/layout.tsx b/frontend/src/app/workspace/layout.tsx index 98b869b4..285aa93e 100644 --- a/frontend/src/app/workspace/layout.tsx +++ b/frontend/src/app/workspace/layout.tsx @@ -76,4 +76,4 @@ export default function WorkspaceLayout({ /> ); -} +} \ No newline at end of file diff --git a/frontend/src/components/ai-elements/artifact.tsx b/frontend/src/components/ai-elements/artifact.tsx index 4867b22a..03ebd13f 100644 --- a/frontend/src/components/ai-elements/artifact.tsx +++ b/frontend/src/components/ai-elements/artifact.tsx @@ -16,7 +16,7 @@ export type ArtifactProps = HTMLAttributes; export const Artifact = ({ className, ...props }: ArtifactProps) => (
(
{ contentClassName?: string; } +function ChevronDownIcon() { + return ( + + + + ); +} + +function ChevronUpIcon() { + return ( + + + + ); +} + export function DropdownSelector({ value, options, @@ -27,16 +69,20 @@ export function DropdownSelector({ contentClassName, }: DropdownSelectorProps) { const selectedOption = options.find((opt) => opt.value === value); + const [isOpen, setIsOpen] = useState(false); return ( - + - {selectedOption?.label ?? value} + + {selectedOption?.label ?? value} + {isOpen ? : } + { return filepathFromProps.startsWith("write-file:"); }, [filepathFromProps]); @@ -111,33 +111,14 @@ export function ArtifactFileDetail({ const [viewMode, setViewMode] = useState<"code" | "preview">("code"); const [isInstalling, setIsInstalling] = useState(false); - const [zoom, setZoom] = useState(100); + const [zoom, setZoom] = useState(80); // 全屏切换处理 const handleFullscreenToggle = useCallback(() => { - if (!document.fullscreenElement) { - document.documentElement.requestFullscreen().catch((err) => { - console.error("无法进入全屏模式:", err); - }); - setFullscreen(true); - } else { - document.exitFullscreen().catch((err) => { - console.error("无法退出全屏模式:", err); - }); - setFullscreen(false); - } - }, [setFullscreen]); - - // 监听全屏变化 - useEffect(() => { - const handleFullscreenChange = () => { - setFullscreen(!!document.fullscreenElement); - }; - document.addEventListener("fullscreenchange", handleFullscreenChange); - return () => { - document.removeEventListener("fullscreenchange", handleFullscreenChange); - }; - }, [setFullscreen]); + const newFullscreen = !fullscreen; + setFullscreen(newFullscreen); + sendToParent({ type: POST_MESSAGE_TYPES.FULLSCREEN, fullscreen: newFullscreen }); + }, [fullscreen, setFullscreen]); useEffect(() => { if (previewable) { @@ -175,8 +156,9 @@ export function ArtifactFileDetail({ {previewable && ( { if (value) { @@ -185,13 +167,23 @@ export function ArtifactFileDetail({ }} > - + + + + + + - + + + + )} + {/* 放大缩小选择器 */} +
@@ -207,8 +199,7 @@ export function ArtifactFileDetail({
- {/* 放大缩小选择器 */} - + {isCodeFile && ( )} - setOpen(false)} - tooltip={t.common.close} - > - setOpen(false)} + tooltip={t.common.close} > - - - + + + + + )}
@@ -471,8 +464,7 @@ export const ArtifactZoomSelector = ({ return (
- + + + + + @@ -505,14 +501,18 @@ export const ArtifactZoomSelector = ({ onClick={handleZoomOut} disabled={!canZoomOut} className={cn( - "flex h-6 w-6 items-center justify-center rounded transition-colors", + "flex h-full w-10 items-center justify-center rounded transition-colors", "text-gray-400 hover:bg-gray-100 hover:text-gray-600", "disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent", "dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300", )} aria-label="缩小" > - + + + + +
); diff --git a/frontend/src/components/workspace/messages/message-list.tsx b/frontend/src/components/workspace/messages/message-list.tsx index db37d275..40bbd391 100644 --- a/frontend/src/components/workspace/messages/message-list.tsx +++ b/frontend/src/components/workspace/messages/message-list.tsx @@ -57,7 +57,7 @@ export function MessageList({ - + {groupMessages(messages, (group) => { if (group.type === "human" || group.type === "assistant") { return ( diff --git a/frontend/src/core/iframe-messages.ts b/frontend/src/core/iframe-messages.ts new file mode 100644 index 00000000..d57849a7 --- /dev/null +++ b/frontend/src/core/iframe-messages.ts @@ -0,0 +1,55 @@ +/** + * iframe 与宿主页通信消息类型常量 + * + * 消息格式:{ type: MESSAGE_TYPE, ...其他字段 } + * 发送方式:window.parent.postMessage(message, "*") + */ + +// 发送给宿主页的消息类型 +export const POST_MESSAGE_TYPES = { + // 全屏切换 + FULLSCREEN: "fullscreen", + // 选择预定义 skill + SELECT_SKILL: "selectSkill", + // 打开 skill 选择对话框 + OPEN_SKILL_DIALOG: "openSkillDialog", +} as const; + +// 接收来自宿主页的消息类型 +export const RECEIVE_MESSAGE_TYPES = { + // 选中的 skill 数据 + SELECTED_SKILL: "selectedSkill", +} as const; + +// 消息类型 +export type PostMessageType = (typeof POST_MESSAGE_TYPES)[keyof typeof POST_MESSAGE_TYPES]; +export type ReceiveMessageType = (typeof RECEIVE_MESSAGE_TYPES)[keyof typeof RECEIVE_MESSAGE_TYPES]; + +// 消息数据类型 +export interface FullscreenMessage { + type: typeof POST_MESSAGE_TYPES.FULLSCREEN; + fullscreen: boolean; +} + +export interface SelectSkillMessage { + type: typeof POST_MESSAGE_TYPES.SELECT_SKILL; + skill_id: string; +} + +export interface OpenSkillDialogMessage { + type: typeof POST_MESSAGE_TYPES.OPEN_SKILL_DIALOG; + openSkillDialog: true; +} + +export interface SelectedSkillMessage { + type: typeof RECEIVE_MESSAGE_TYPES.SELECTED_SKILL; + id: string | number; + title: string; +} + +// 发送消息的辅助函数 +export function sendToParent(message: FullscreenMessage | SelectSkillMessage | OpenSkillDialogMessage): void { + if (window.parent !== window) { + window.parent.postMessage(message, "*"); + } +} \ No newline at end of file diff --git a/frontend/src/hooks/use-iframe-skill.ts b/frontend/src/hooks/use-iframe-skill.ts index a5b9e9ec..63cd5cee 100644 --- a/frontend/src/hooks/use-iframe-skill.ts +++ b/frontend/src/hooks/use-iframe-skill.ts @@ -1,11 +1,12 @@ import { useSearchParams } from "next/navigation"; import { useState, useEffect, useCallback } from "react"; -// 消息类型常量 -const MESSAGE_TYPES = { - SELECT_SKILL: "selectSkill", - OPEN_SKILL_DIALOG: "openSkillDialog", -} as const; +import { + POST_MESSAGE_TYPES, + RECEIVE_MESSAGE_TYPES, + sendToParent, + type SelectedSkillMessage, +} from "@/core/iframe-messages"; // Skill 数据类型 interface SkillData { @@ -38,8 +39,8 @@ export function useIframeSkill(): UseIframeSkillReturn { // 2. 监听宿主页 postMessage useEffect(() => { const handleMessage = (event: MessageEvent) => { - if (event.data?.type === "selectedSkill") { - const { id, title } = event.data; + if (event.data?.type === RECEIVE_MESSAGE_TYPES.SELECTED_SKILL) { + const { id, title } = event.data as SelectedSkillMessage; setSelectedSkill({ skill_id: String(id), title }); } }; @@ -49,28 +50,28 @@ export function useIframeSkill(): UseIframeSkillReturn { // 发送选择预定义 skill const sendSelectSkill = useCallback((skill_id: string) => { - const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id }; + const message = { type: POST_MESSAGE_TYPES.SELECT_SKILL, skill_id }; console.log("[useIframeSkill] sendSelectSkill:", message); - window.parent.postMessage(message, "*"); + sendToParent(message); }, []); // 打开 skill 选择对话框 const openSkillDialog = useCallback(() => { const message = { - type: MESSAGE_TYPES.OPEN_SKILL_DIALOG, + type: POST_MESSAGE_TYPES.OPEN_SKILL_DIALOG, openSkillDialog: true, - }; + } as const; console.log("[useIframeSkill] openSkillDialog:", message); - window.parent.postMessage(message, "*"); + sendToParent(message); }, []); // 清除选中并发送 skill_id=0 给主页 const clearSkill = useCallback(() => { setSelectedSkill(null); // 发送 skill_id=0 给主页,通知取消选择 - const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id: "0" }; + const message = { type: POST_MESSAGE_TYPES.SELECT_SKILL, skill_id: "0" }; console.log("[useIframeSkill] clearSkill, sending skill_id=0:", message); - window.parent.postMessage(message, "*"); + sendToParent(message); }, []); return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill };