feat(ui): 重构聊天页布局并规范化iframe 通信
This commit is contained in:
parent
3a7940654c
commit
cb0ebf41bb
|
|
@ -8,6 +8,8 @@
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"check": "next lint && tsc --noEmit",
|
"check": "next lint && tsc --noEmit",
|
||||||
"dev": "next dev --turbo",
|
"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": "eslint . --ext .ts,.tsx",
|
||||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||||
"preview": "next build && next start",
|
"preview": "next build && next start",
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ export default async function RootLayout({
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang={locale}
|
lang={locale}
|
||||||
className={geist.variable}
|
className={geist.variable+""}
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,4 @@ export default function ChatLayout({
|
||||||
</ArtifactsProvider>
|
</ArtifactsProvider>
|
||||||
</SubtasksProvider>
|
</SubtasksProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -17,11 +17,6 @@ import {
|
||||||
DevDialogHeader,
|
DevDialogHeader,
|
||||||
DevDialogTitle,
|
DevDialogTitle,
|
||||||
} from "@/components/ui/dev-dialog";
|
} from "@/components/ui/dev-dialog";
|
||||||
import {
|
|
||||||
ResizableHandle,
|
|
||||||
ResizablePanel,
|
|
||||||
ResizablePanelGroup,
|
|
||||||
} from "@/components/ui/resizable";
|
|
||||||
import { useSidebar } from "@/components/ui/sidebar";
|
import { useSidebar } from "@/components/ui/sidebar";
|
||||||
import {
|
import {
|
||||||
ArtifactFileDetail,
|
ArtifactFileDetail,
|
||||||
|
|
@ -257,13 +252,19 @@ export default function ChatPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThreadContext.Provider value={{ threadId, thread }}>
|
<ThreadContext.Provider value={{ threadId, thread }}>
|
||||||
<ResizablePanelGroup orientation="horizontal">
|
<div className={cn(
|
||||||
<ResizablePanel
|
"m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out",
|
||||||
className="relative overflow-hidden rounded-[20px]"
|
artifactsOpen ? "w-full" : "w-[50%]",
|
||||||
defaultSize={artifactPanelOpen ? 46 : 100}
|
)}>
|
||||||
minSize={artifactPanelOpen ? 30 : 100}
|
<div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]">
|
||||||
>
|
<div
|
||||||
<div className="relative flex size-full min-h-0 justify-between rounded-[20px]">
|
className={cn(
|
||||||
|
"relative overflow-hidden rounded-t-[20px] transition-all duration-300 ease-in-out",
|
||||||
|
artifactPanelOpen ? "w-[50%]" : "w-full",
|
||||||
|
fullscreen && "hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]">
|
||||||
<header
|
<header
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background absolute top-0 right-0 left-0 z-30 mx-4 grid h-[58px] shrink-0 grid-cols-3 items-center border-b transition-all duration-300 ease-in-out",
|
"bg-background absolute top-0 right-0 left-0 z-30 mx-4 grid h-[58px] shrink-0 grid-cols-3 items-center border-b transition-all duration-300 ease-in-out",
|
||||||
|
|
@ -294,7 +295,7 @@ export default function ChatPage() {
|
||||||
</svg>
|
</svg>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center overflow-hidden text-sm font-medium">
|
<div className="flex items-center justify-center whitespace-nowrap text-[#333333] font-bold overflow-hidden text-sm font-medium">
|
||||||
{title !== "Untitled" && (
|
{title !== "Untitled" && (
|
||||||
<ThreadTitle threadId={threadId} threadTitle={title} />
|
<ThreadTitle threadId={threadId} threadTitle={title} />
|
||||||
)}
|
)}
|
||||||
|
|
@ -336,7 +337,7 @@ export default function ChatPage() {
|
||||||
</header>
|
</header>
|
||||||
<main
|
<main
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 max-w-full grow flex-col rounded-[20px]",
|
"flex min-h-0 max-w-full grow flex-col",
|
||||||
isNewThread && !hasSubmitted ? "bg-white" : "bg-background",
|
isNewThread && !hasSubmitted ? "bg-white" : "bg-background",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -358,21 +359,13 @@ export default function ChatPage() {
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</div>
|
||||||
<ResizableHandle
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"opacity-33 hover:opacity-100",
|
"bg-background ml-[20px] rounded-t-[20px] transition-all duration-300 ease-in-out",
|
||||||
!artifactPanelOpen && "pointer-events-none opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<ResizablePanel
|
|
||||||
className={cn(
|
|
||||||
"bg-background ml-[20px] rounded-[20px] transition-all duration-300 ease-in-out",
|
|
||||||
!artifactsOpen && "opacity-0",
|
!artifactsOpen && "opacity-0",
|
||||||
|
artifactPanelOpen ? (fullscreen ? "w-full ml-0" : "w-[70%]") : "w-0",
|
||||||
)}
|
)}
|
||||||
defaultSize={artifactPanelOpen ? 64 : 0}
|
|
||||||
minSize={0}
|
|
||||||
maxSize={artifactPanelOpen ? undefined : 0}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -388,7 +381,7 @@ export default function ChatPage() {
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative flex size-full justify-center px-[20px]">
|
<div className="relative flex size-full justify-center px-[20px]">
|
||||||
<div className="absolute top-1 right-1 z-30">
|
<div className="absolute top-2 right-2 z-30">
|
||||||
<Button
|
<Button
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -406,9 +399,9 @@ export default function ChatPage() {
|
||||||
description="Select an artifact to view its details"
|
description="Select an artifact to view its details"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4 pt-8">
|
<div className="flex size-full max-w-(--container-width-sm) flex-col justify-center p-4">
|
||||||
<header className="shrink-0">
|
<header className="shrink-0">
|
||||||
<h2 className="text-lg font-medium">
|
<h2 className="text-[14px] text-[#333333] font-bold">
|
||||||
{t.common.artifacts}
|
{t.common.artifacts}
|
||||||
</h2>
|
</h2>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -424,21 +417,21 @@ export default function ChatPage() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</div>
|
||||||
</ResizablePanelGroup>
|
</div>
|
||||||
|
|
||||||
{/* Fixed 底部居中输入框容器 */}
|
{/* Fixed 底部居中输入框容器 */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none fixed right-0 bottom-3 left-0 z-30 flex justify-center px-4",
|
"pointer-events-none fixed right-0 bottom-3 left-0 z-30 flex justify-center px-4",
|
||||||
"transition-all duration-300 ease-in-out",
|
"transition-all duration-300 ease-in-out",
|
||||||
fullscreen ? "right-[50%]" : "",
|
fullscreen ? "hidden" : "",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-auto relative w-full max-w-[720px]",
|
"pointer-events-auto relative w-full max-w-[720px]",
|
||||||
isNewThread && !hasSubmitted && "top-[-65px] -translate-y-[calc(50vh-96px)]",
|
isNewThread && !hasSubmitted && "-translate-y-[calc(50vh-96px)]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<InputBox
|
<InputBox
|
||||||
|
|
@ -541,6 +534,7 @@ export default function ChatPage() {
|
||||||
|
|
||||||
{/* MARK: 开发测试:iframe 通信功能测试面板 */}
|
{/* MARK: 开发测试:iframe 通信功能测试面板 */}
|
||||||
{/* <IframeTestPanel /> */}
|
{/* <IframeTestPanel /> */}
|
||||||
|
</div>
|
||||||
</ThreadContext.Provider>
|
</ThreadContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,4 +76,4 @@ export default function WorkspaceLayout({
|
||||||
/>
|
/>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
||||||
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-background flex flex-col overflow-hidden rounded-[20px] px-[20px] pt-[15px]",
|
"bg-background flex flex-col overflow-hidden rounded-t-[20px] px-[20px] pt-[15px]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -31,7 +31,7 @@ export const ArtifactHeader = ({
|
||||||
}: ArtifactHeaderProps) => (
|
}: ArtifactHeaderProps) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-[20px] grid grid-cols-3 items-center justify-between",
|
"mb-[10px] flex items-center justify-between",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
|
|
@ -19,6 +21,46 @@ interface DropdownSelectorProps<T extends string> {
|
||||||
contentClassName?: string;
|
contentClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ChevronDownIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="6"
|
||||||
|
viewBox="0 0 10 6"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0.75 0.75L4.75 4.75L8.75 0.75"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChevronUpIcon() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="6"
|
||||||
|
viewBox="0 0 10 6"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0.75 4.75L4.75 0.75L8.75 4.75"
|
||||||
|
stroke="#666666"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function DropdownSelector<T extends string>({
|
export function DropdownSelector<T extends string>({
|
||||||
value,
|
value,
|
||||||
options,
|
options,
|
||||||
|
|
@ -27,16 +69,20 @@ export function DropdownSelector<T extends string>({
|
||||||
contentClassName,
|
contentClassName,
|
||||||
}: DropdownSelectorProps<T>) {
|
}: DropdownSelectorProps<T>) {
|
||||||
const selectedOption = options.find((opt) => opt.value === value);
|
const selectedOption = options.find((opt) => opt.value === value);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DropdownMenuTrigger
|
<DropdownMenuTrigger
|
||||||
className={
|
className={
|
||||||
triggerClassName ??
|
triggerClassName ??
|
||||||
"border-none bg-transparent shadow-none select-none focus:outline-none"
|
"border-none bg-transparent shadow-none select-none focus:outline-none"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{selectedOption?.label ?? value}
|
<span className="flex items-center gap-1">
|
||||||
|
{selectedOption?.label ?? value}
|
||||||
|
{isOpen ? <ChevronUpIcon /> : <ChevronDownIcon />}
|
||||||
|
</span>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className={contentClassName}>
|
<DropdownMenuContent className={contentClassName}>
|
||||||
<DropdownMenuRadioGroup
|
<DropdownMenuRadioGroup
|
||||||
|
|
|
||||||
|
|
@ -69,8 +69,8 @@ function ToggleGroupItem({
|
||||||
variant: context.variant || variant,
|
variant: context.variant || variant,
|
||||||
size: context.size || size,
|
size: context.size || size,
|
||||||
}),
|
}),
|
||||||
"w-auto min-w-0 shrink-0 cursor-pointer px-3 focus:z-10 focus-visible:z-10",
|
"w-[50px] h-full min-w-0 shrink-0 bg-white cursor-pointer px-3 focus:z-10 focus-visible:z-10",
|
||||||
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
|
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import { CodeEditor } from "@/components/workspace/code-editor";
|
||||||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
|
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||||
import { installSkill } from "@/core/skills/api";
|
import { installSkill } from "@/core/skills/api";
|
||||||
import { streamdownPlugins } from "@/core/streamdown";
|
import { streamdownPlugins } from "@/core/streamdown";
|
||||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||||
|
|
@ -64,8 +65,7 @@ export function ArtifactFileDetail({
|
||||||
threadId: string;
|
threadId: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { artifacts, setOpen, select } = useArtifacts();
|
const { artifacts, setOpen, select, fullscreen, setFullscreen } = useArtifacts();
|
||||||
const [fullscreen, setFullscreen] = useState(false);
|
|
||||||
const isWriteFile = useMemo(() => {
|
const isWriteFile = useMemo(() => {
|
||||||
return filepathFromProps.startsWith("write-file:");
|
return filepathFromProps.startsWith("write-file:");
|
||||||
}, [filepathFromProps]);
|
}, [filepathFromProps]);
|
||||||
|
|
@ -111,33 +111,14 @@ export function ArtifactFileDetail({
|
||||||
|
|
||||||
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
|
||||||
const [isInstalling, setIsInstalling] = useState(false);
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
const [zoom, setZoom] = useState(100);
|
const [zoom, setZoom] = useState(80);
|
||||||
|
|
||||||
// 全屏切换处理
|
// 全屏切换处理
|
||||||
const handleFullscreenToggle = useCallback(() => {
|
const handleFullscreenToggle = useCallback(() => {
|
||||||
if (!document.fullscreenElement) {
|
const newFullscreen = !fullscreen;
|
||||||
document.documentElement.requestFullscreen().catch((err) => {
|
setFullscreen(newFullscreen);
|
||||||
console.error("无法进入全屏模式:", err);
|
sendToParent({ type: POST_MESSAGE_TYPES.FULLSCREEN, fullscreen: newFullscreen });
|
||||||
});
|
}, [fullscreen, setFullscreen]);
|
||||||
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]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previewable) {
|
if (previewable) {
|
||||||
|
|
@ -175,8 +156,9 @@ export function ArtifactFileDetail({
|
||||||
{previewable && (
|
{previewable && (
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
variant="outline"
|
variant={null}
|
||||||
size="sm"
|
size="default"
|
||||||
|
className="h-[28px]"
|
||||||
value={viewMode}
|
value={viewMode}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|
@ -185,13 +167,23 @@ export function ArtifactFileDetail({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ToggleGroupItem value="code">
|
<ToggleGroupItem value="code">
|
||||||
<Code2Icon />
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5 6L2 9L5 12" stroke="#150033" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M11 3L7 15" stroke="#150033" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13 6L16 9L13 12" stroke="#150033" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="preview">
|
<ToggleGroupItem value="preview">
|
||||||
<EyeIcon />
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="10" viewBox="0 0 16 10" fill="none">
|
||||||
|
<path d="M8 0.5C10.4943 0.5 12.8473 1.84466 14.792 4.21973C15.1644 4.67466 15.1644 5.32534 14.792 5.78027C12.8473 8.15534 10.4943 9.5 8 9.5C5.50561 9.49989 3.15269 8.15543 1.20801 5.78027C0.835561 5.32534 0.835562 4.67466 1.20801 4.21973C3.15269 1.84457 5.50561 0.500106 8 0.5Z" stroke="#666666"/>
|
||||||
|
<circle cx="8" cy="5" r="1.5" stroke="#666666"/>
|
||||||
|
</svg>
|
||||||
</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
)}
|
)}
|
||||||
|
{/* 放大缩小选择器 */}
|
||||||
|
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-w-0 grow items-center justify-center">
|
<div className="flex min-w-0 grow items-center justify-center">
|
||||||
<ArtifactTitle>
|
<ArtifactTitle>
|
||||||
|
|
@ -207,8 +199,7 @@ export function ArtifactFileDetail({
|
||||||
</ArtifactTitle>
|
</ArtifactTitle>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-end overflow-hidden">
|
<div className="flex items-center justify-end overflow-hidden">
|
||||||
{/* 放大缩小选择器 */}
|
|
||||||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
|
||||||
<ArtifactActions>
|
<ArtifactActions>
|
||||||
{isCodeFile && (
|
{isCodeFile && (
|
||||||
<ArtifactAction
|
<ArtifactAction
|
||||||
|
|
@ -340,25 +331,27 @@ export function ArtifactFileDetail({
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</ArtifactAction>
|
</ArtifactAction>
|
||||||
<ArtifactAction
|
{!fullscreen && (
|
||||||
label={t.common.close}
|
<ArtifactAction
|
||||||
onClick={() => setOpen(false)}
|
label={t.common.close}
|
||||||
tooltip={t.common.close}
|
onClick={() => setOpen(false)}
|
||||||
>
|
tooltip={t.common.close}
|
||||||
<svg
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
viewBox="0 0 18 18"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="M4 14L14 4M4 4L14 14"
|
width="18"
|
||||||
stroke="#666666"
|
height="18"
|
||||||
stroke-linecap="round"
|
viewBox="0 0 18 18"
|
||||||
/>
|
fill="none"
|
||||||
</svg>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</ArtifactAction>
|
>
|
||||||
|
<path
|
||||||
|
d="M4 14L14 4M4 4L14 14"
|
||||||
|
stroke="#666666"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</ArtifactAction>
|
||||||
|
)}
|
||||||
</ArtifactActions>
|
</ArtifactActions>
|
||||||
</div>
|
</div>
|
||||||
</ArtifactHeader>
|
</ArtifactHeader>
|
||||||
|
|
@ -471,8 +464,7 @@ export const ArtifactZoomSelector = ({
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex items-center gap-2 rounded-[10px] bg-white px-2 py-1 backdrop-blur-sm",
|
"inline-flex items-center gap-1 rounded-[10px] h-[28px] bg-white backdrop-blur-sm",
|
||||||
"border border-gray-200/50",
|
|
||||||
"dark:border-gray-700/50 dark:bg-gray-800/90",
|
"dark:border-gray-700/50 dark:bg-gray-800/90",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
@ -483,18 +475,22 @@ export const ArtifactZoomSelector = ({
|
||||||
onClick={handleZoomIn}
|
onClick={handleZoomIn}
|
||||||
disabled={!canZoomIn}
|
disabled={!canZoomIn}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-6 w-6 items-center justify-center rounded transition-colors",
|
"flex h-full w-10 py-1 items-center justify-center rounded transition-colors",
|
||||||
"text-gray-400 hover:bg-gray-100 hover:text-gray-600",
|
"text-gray-400 hover:bg-gray-100 hover:text-gray-600",
|
||||||
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
"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",
|
"dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300",
|
||||||
)}
|
)}
|
||||||
aria-label="放大"
|
aria-label="放大"
|
||||||
>
|
>
|
||||||
<ZoomIn className="h-3.5 w-3.5" />
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666"/>
|
||||||
|
<path d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z" fill="#666666"/>
|
||||||
|
<path d="M5.33325 7.5H9.7777M7.55547 5V10" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-[42px] text-center text-xs font-medium text-gray-600",
|
"min-w-[36px] text-center text-xs font-medium text-gray-600",
|
||||||
"dark:text-gray-300",
|
"dark:text-gray-300",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -505,14 +501,18 @@ export const ArtifactZoomSelector = ({
|
||||||
onClick={handleZoomOut}
|
onClick={handleZoomOut}
|
||||||
disabled={!canZoomOut}
|
disabled={!canZoomOut}
|
||||||
className={cn(
|
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",
|
"text-gray-400 hover:bg-gray-100 hover:text-gray-600",
|
||||||
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent",
|
"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",
|
"dark:text-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-300",
|
||||||
)}
|
)}
|
||||||
aria-label="缩小"
|
aria-label="缩小"
|
||||||
>
|
>
|
||||||
<ZoomOut className="h-3.5 w-3.5" />
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||||
|
<circle cx="7.55558" cy="7.55534" r="6.16667" stroke="#666666"/>
|
||||||
|
<path d="M13.8688 15.4646C14.064 15.6598 14.3806 15.6598 14.5759 15.4646C14.7711 15.2693 14.7711 14.9527 14.5759 14.7574L14.2223 15.111L13.8688 15.4646ZM14.2223 15.111L14.5759 14.7574L11.9092 12.0908L11.5557 12.4443L11.2021 12.7979L13.8688 15.4646L14.2223 15.111Z" fill="#666666"/>
|
||||||
|
<path d="M4.99927 7.5H9.99927" stroke="#666666" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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="mx-auto w-full max-w-(--container-width-md) gap-8 pt-12">
|
<ConversationContent className="px-[20px] w-full gap-8 pt-12">
|
||||||
{groupMessages(messages, (group) => {
|
{groupMessages(messages, (group) => {
|
||||||
if (group.type === "human" || group.type === "assistant") {
|
if (group.type === "human" || group.type === "assistant") {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -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, "*");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
// 消息类型常量
|
import {
|
||||||
const MESSAGE_TYPES = {
|
POST_MESSAGE_TYPES,
|
||||||
SELECT_SKILL: "selectSkill",
|
RECEIVE_MESSAGE_TYPES,
|
||||||
OPEN_SKILL_DIALOG: "openSkillDialog",
|
sendToParent,
|
||||||
} as const;
|
type SelectedSkillMessage,
|
||||||
|
} from "@/core/iframe-messages";
|
||||||
|
|
||||||
// Skill 数据类型
|
// Skill 数据类型
|
||||||
interface SkillData {
|
interface SkillData {
|
||||||
|
|
@ -38,8 +39,8 @@ export function useIframeSkill(): UseIframeSkillReturn {
|
||||||
// 2. 监听宿主页 postMessage
|
// 2. 监听宿主页 postMessage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleMessage = (event: MessageEvent) => {
|
const handleMessage = (event: MessageEvent) => {
|
||||||
if (event.data?.type === "selectedSkill") {
|
if (event.data?.type === RECEIVE_MESSAGE_TYPES.SELECTED_SKILL) {
|
||||||
const { id, title } = event.data;
|
const { id, title } = event.data as SelectedSkillMessage;
|
||||||
setSelectedSkill({ skill_id: String(id), title });
|
setSelectedSkill({ skill_id: String(id), title });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -49,28 +50,28 @@ export function useIframeSkill(): UseIframeSkillReturn {
|
||||||
|
|
||||||
// 发送选择预定义 skill
|
// 发送选择预定义 skill
|
||||||
const sendSelectSkill = useCallback((skill_id: string) => {
|
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);
|
console.log("[useIframeSkill] sendSelectSkill:", message);
|
||||||
window.parent.postMessage(message, "*");
|
sendToParent(message);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 打开 skill 选择对话框
|
// 打开 skill 选择对话框
|
||||||
const openSkillDialog = useCallback(() => {
|
const openSkillDialog = useCallback(() => {
|
||||||
const message = {
|
const message = {
|
||||||
type: MESSAGE_TYPES.OPEN_SKILL_DIALOG,
|
type: POST_MESSAGE_TYPES.OPEN_SKILL_DIALOG,
|
||||||
openSkillDialog: true,
|
openSkillDialog: true,
|
||||||
};
|
} as const;
|
||||||
console.log("[useIframeSkill] openSkillDialog:", message);
|
console.log("[useIframeSkill] openSkillDialog:", message);
|
||||||
window.parent.postMessage(message, "*");
|
sendToParent(message);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 清除选中并发送 skill_id=0 给主页
|
// 清除选中并发送 skill_id=0 给主页
|
||||||
const clearSkill = useCallback(() => {
|
const clearSkill = useCallback(() => {
|
||||||
setSelectedSkill(null);
|
setSelectedSkill(null);
|
||||||
// 发送 skill_id=0 给主页,通知取消选择
|
// 发送 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);
|
console.log("[useIframeSkill] clearSkill, sending skill_id=0:", message);
|
||||||
window.parent.postMessage(message, "*");
|
sendToParent(message);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill };
|
return { selectedSkill, sendSelectSkill, openSkillDialog, clearSkill };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue