feat(ui): 重构聊天页布局并规范化iframe 通信
This commit is contained in:
parent
3a7940654c
commit
cb0ebf41bb
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default async function RootLayout({
|
|||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
className={geist.variable}
|
||||
className={geist.variable+""}
|
||||
suppressContentEditableWarning
|
||||
suppressHydrationWarning
|
||||
>
|
||||
|
|
|
|||
|
|
@ -16,4 +16,4 @@ export default function ChatLayout({
|
|||
</ArtifactsProvider>
|
||||
</SubtasksProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<ThreadContext.Provider value={{ threadId, thread }}>
|
||||
<ResizablePanelGroup orientation="horizontal">
|
||||
<ResizablePanel
|
||||
className="relative overflow-hidden rounded-[20px]"
|
||||
defaultSize={artifactPanelOpen ? 46 : 100}
|
||||
minSize={artifactPanelOpen ? 30 : 100}
|
||||
>
|
||||
<div className="relative flex size-full min-h-0 justify-between rounded-[20px]">
|
||||
<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%]",
|
||||
)}>
|
||||
<div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]">
|
||||
<div
|
||||
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
|
||||
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",
|
||||
|
|
@ -294,7 +295,7 @@ export default function ChatPage() {
|
|||
</svg>
|
||||
</Button>
|
||||
</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" && (
|
||||
<ThreadTitle threadId={threadId} threadTitle={title} />
|
||||
)}
|
||||
|
|
@ -336,7 +337,7 @@ export default function ChatPage() {
|
|||
</header>
|
||||
<main
|
||||
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",
|
||||
)}
|
||||
>
|
||||
|
|
@ -358,21 +359,13 @@ export default function ChatPage() {
|
|||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"opacity-33 hover:opacity-100",
|
||||
!artifactPanelOpen && "pointer-events-none opacity-0",
|
||||
)}
|
||||
/>
|
||||
<ResizablePanel
|
||||
className={cn(
|
||||
"bg-background ml-[20px] rounded-[20px] transition-all duration-300 ease-in-out",
|
||||
"bg-background ml-[20px] rounded-t-[20px] transition-all duration-300 ease-in-out",
|
||||
!artifactsOpen && "opacity-0",
|
||||
artifactPanelOpen ? (fullscreen ? "w-full ml-0" : "w-[70%]") : "w-0",
|
||||
)}
|
||||
defaultSize={artifactPanelOpen ? 64 : 0}
|
||||
minSize={0}
|
||||
maxSize={artifactPanelOpen ? undefined : 0}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -388,7 +381,7 @@ export default function ChatPage() {
|
|||
/>
|
||||
) : (
|
||||
<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
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
|
|
@ -406,9 +399,9 @@ export default function ChatPage() {
|
|||
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">
|
||||
<h2 className="text-lg font-medium">
|
||||
<h2 className="text-[14px] text-[#333333] font-bold">
|
||||
{t.common.artifacts}
|
||||
</h2>
|
||||
</header>
|
||||
|
|
@ -424,21 +417,21 @@ export default function ChatPage() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed 底部居中输入框容器 */}
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none fixed right-0 bottom-3 left-0 z-30 flex justify-center px-4",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
fullscreen ? "right-[50%]" : "",
|
||||
fullscreen ? "hidden" : "",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"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
|
||||
|
|
@ -541,6 +534,7 @@ export default function ChatPage() {
|
|||
|
||||
{/* MARK: 开发测试:iframe 通信功能测试面板 */}
|
||||
{/* <IframeTestPanel /> */}
|
||||
</div>
|
||||
</ThreadContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,4 +76,4 @@ export default function WorkspaceLayout({
|
|||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ export type ArtifactProps = HTMLAttributes<HTMLDivElement>;
|
|||
export const Artifact = ({ className, ...props }: ArtifactProps) => (
|
||||
<div
|
||||
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,
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -31,7 +31,7 @@ export const ArtifactHeader = ({
|
|||
}: ArtifactHeaderProps) => (
|
||||
<div
|
||||
className={cn(
|
||||
"mb-[20px] grid grid-cols-3 items-center justify-between",
|
||||
"mb-[10px] flex items-center justify-between",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -19,6 +21,46 @@ interface DropdownSelectorProps<T extends 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>({
|
||||
value,
|
||||
options,
|
||||
|
|
@ -27,16 +69,20 @@ export function DropdownSelector<T extends string>({
|
|||
contentClassName,
|
||||
}: DropdownSelectorProps<T>) {
|
||||
const selectedOption = options.find((opt) => opt.value === value);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DropdownMenuTrigger
|
||||
className={
|
||||
triggerClassName ??
|
||||
"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>
|
||||
<DropdownMenuContent className={contentClassName}>
|
||||
<DropdownMenuRadioGroup
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ function ToggleGroupItem({
|
|||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"w-auto min-w-0 shrink-0 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",
|
||||
"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]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import { CodeEditor } from "@/components/workspace/code-editor";
|
|||
import { useArtifactContent } from "@/core/artifacts/hooks";
|
||||
import { urlOfArtifact } from "@/core/artifacts/utils";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { POST_MESSAGE_TYPES, sendToParent } from "@/core/iframe-messages";
|
||||
import { installSkill } from "@/core/skills/api";
|
||||
import { streamdownPlugins } from "@/core/streamdown";
|
||||
import { checkCodeFile, getFileName } from "@/core/utils/files";
|
||||
|
|
@ -64,8 +65,7 @@ export function ArtifactFileDetail({
|
|||
threadId: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const { artifacts, setOpen, select } = useArtifacts();
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const { artifacts, setOpen, select, fullscreen, setFullscreen } = useArtifacts();
|
||||
const isWriteFile = useMemo(() => {
|
||||
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 && (
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
variant={null}
|
||||
size="default"
|
||||
className="h-[28px]"
|
||||
value={viewMode}
|
||||
onValueChange={(value) => {
|
||||
if (value) {
|
||||
|
|
@ -185,13 +167,23 @@ export function ArtifactFileDetail({
|
|||
}}
|
||||
>
|
||||
<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 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>
|
||||
</ToggleGroup>
|
||||
)}
|
||||
{/* 放大缩小选择器 */}
|
||||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||
</div>
|
||||
<div className="flex min-w-0 grow items-center justify-center">
|
||||
<ArtifactTitle>
|
||||
|
|
@ -207,8 +199,7 @@ export function ArtifactFileDetail({
|
|||
</ArtifactTitle>
|
||||
</div>
|
||||
<div className="flex items-center justify-end overflow-hidden">
|
||||
{/* 放大缩小选择器 */}
|
||||
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
|
||||
|
||||
<ArtifactActions>
|
||||
{isCodeFile && (
|
||||
<ArtifactAction
|
||||
|
|
@ -340,25 +331,27 @@ export function ArtifactFileDetail({
|
|||
</svg>
|
||||
)}
|
||||
</ArtifactAction>
|
||||
<ArtifactAction
|
||||
label={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"
|
||||
{!fullscreen && (
|
||||
<ArtifactAction
|
||||
label={t.common.close}
|
||||
onClick={() => setOpen(false)}
|
||||
tooltip={t.common.close}
|
||||
>
|
||||
<path
|
||||
d="M4 14L14 4M4 4L14 14"
|
||||
stroke="#666666"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</ArtifactAction>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M4 14L14 4M4 4L14 14"
|
||||
stroke="#666666"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</ArtifactAction>
|
||||
)}
|
||||
</ArtifactActions>
|
||||
</div>
|
||||
</ArtifactHeader>
|
||||
|
|
@ -471,8 +464,7 @@ export const ArtifactZoomSelector = ({
|
|||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"inline-flex items-center gap-2 rounded-[10px] bg-white px-2 py-1 backdrop-blur-sm",
|
||||
"border border-gray-200/50",
|
||||
"inline-flex items-center gap-1 rounded-[10px] h-[28px] bg-white backdrop-blur-sm",
|
||||
"dark:border-gray-700/50 dark:bg-gray-800/90",
|
||||
className,
|
||||
)}
|
||||
|
|
@ -483,18 +475,22 @@ export const ArtifactZoomSelector = ({
|
|||
onClick={handleZoomIn}
|
||||
disabled={!canZoomIn}
|
||||
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",
|
||||
"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="放大"
|
||||
>
|
||||
<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>
|
||||
<span
|
||||
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",
|
||||
)}
|
||||
>
|
||||
|
|
@ -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="缩小"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function MessageList({
|
|||
<Conversation
|
||||
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) => {
|
||||
if (group.type === "human" || group.type === "assistant") {
|
||||
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 { 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 };
|
||||
|
|
|
|||
Loading…
Reference in New Issue