feat:全屏功能

This commit is contained in:
肖应宇 2026-03-17 16:40:02 +08:00
parent a8d1c8367f
commit 32f581cf50
7 changed files with 194 additions and 22 deletions

View File

@ -24,6 +24,7 @@ import { MessageList } from "@/components/workspace/messages";
import { ThreadContext } from "@/components/workspace/messages/context";
import { ThreadTitle } from "@/components/workspace/thread-title";
import { Welcome } from "@/components/workspace/welcome";
import { useArtifacts } from "@/components/workspace/artifacts";
import { useI18n } from "@/core/i18n/hooks";
import { useNotification } from "@/core/notification/hooks";
import { useLocalSettings } from "@/core/settings";
@ -36,6 +37,7 @@ export default function ChatPage() {
const { t } = useI18n();
const [settings, setSettings] = useLocalSettings();
const [showExitDialog, setShowExitDialog] = useState(false);
const { fullscreen } = useArtifacts();
const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat();
useSpecificChatMode();
@ -85,7 +87,7 @@ export default function ChatPage() {
<div className="bg-background relative flex size-full min-h-0 justify-between">
<header
className={cn(
"absolute top-0 right-0 left-0 z-30 grid h-[58px] shrink-0 grid-cols-3 items-center rounded-t-[20px] px-[20px] py-[15px]",
"absolute top-0 right-0 left-0 z-30 mx-[20px] grid h-[58px] shrink-0 grid-cols-3 items-center rounded-t-[20px] border-b py-[15px]",
isNewThread
? "bg-background/0 backdrop-blur-none"
: "bg-background/80 shadow-xs backdrop-blur",
@ -130,7 +132,7 @@ export default function ChatPage() {
variant="ghost"
className="h-full px-[10px] py-[5px] text-sm font-medium"
>
<ListTodoIcon className="size-4" /> Todo
<ListTodoIcon className="size-4" /> To-dos
</Button>
}
/>
@ -147,14 +149,19 @@ export default function ChatPage() {
</div>
</main>
</div>
<div className="pointer-events-none fixed right-0 bottom-3 left-0 z-30 flex justify-center px-4">
<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
? "pointer-events-none translate-y-4 opacity-0"
: "translate-y-0 opacity-100",
)}
>
<div
className={cn(
"pointer-events-auto relative w-full max-w-[720px]",
isNewThread && "top-[-65px] -translate-y-[calc(50vh-96px)]",
// isNewThread
// ? "max-w-(--container-width-sm)"
// : "max-w-(--container-width-md)",
)}
>
<InputBox

View File

@ -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 min-w-(--container-width-sm) flex-col overflow-hidden rounded-[20px] px-[20px] pt-[15px]",
className,
)}
{...props}

View File

@ -15,6 +15,7 @@ import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type HTMLAttributes,
} from "react";
@ -57,7 +58,8 @@ export function ArtifactFileDetail({
threadId: string;
}) {
const { t } = useI18n();
const { artifacts, setOpen, select } = useArtifacts();
const { artifacts, setOpen, select, fullscreen, setFullscreen } =
useArtifacts();
const isWriteFile = useMemo(() => {
return filepathFromProps.startsWith("write-file:");
}, [filepathFromProps]);
@ -105,6 +107,33 @@ export function ArtifactFileDetail({
const [isInstalling, setIsInstalling] = useState(false);
const [zoom, setZoom] = useState(100);
const { isMock } = useThread();
// 全屏切换处理
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]);
useEffect(() => {
if (isSupportPreview) {
setViewMode("preview");
@ -172,11 +201,10 @@ export function ArtifactFileDetail({
)}
</ArtifactTitle>
</div>
<div className="flex items-center justify-end gap-2">
<div className="flex items-center justify-end overflow-hidden">
{/* 放大缩小选择器 */}
<ArtifactZoomSelector value={zoom} onChange={setZoom} />
<ArtifactActions>
{/* 新界面打开的按钮 */}
{/* {!isWriteFile && filepath.endsWith(".skill") && (
<Tooltip content={t.toolCalls.skillInstallTooltip}>
<ArtifactAction
@ -191,7 +219,8 @@ export function ArtifactFileDetail({
/>
</Tooltip>
)} */}
{!isWriteFile && (
{/* 新界面打开的按钮 */}
{/* {!isWriteFile && (
<a href={urlOfArtifact({ filepath, threadId })} target="_blank">
<ArtifactAction
icon={SquareArrowOutUpRightIcon}
@ -199,10 +228,10 @@ export function ArtifactFileDetail({
tooltip={t.common.openInNewWindow}
/>
</a>
)}
)} */}
{/* 复制按钮 */}
{isCodeFile && (
<ArtifactAction
icon={CopyIcon}
label={t.clipboard.copyToClipboard}
disabled={!content}
onClick={async () => {
@ -215,26 +244,143 @@ export function ArtifactFileDetail({
}
}}
tooltip={t.clipboard.copyToClipboard}
/>
>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 2H13C14.1046 2 15 2.89543 15 4V13"
stroke="#666666"
stroke-linecap="round"
stroke-linejoin="round"
/>
<rect
x="2.5"
y="4.5"
width="10"
height="11"
rx="1.5"
stroke="#666666"
/>
</svg>
</ArtifactAction>
)}
{/* 下载按钮 */}
{!isWriteFile && (
<a
href={urlOfArtifact({ filepath, threadId, download: true })}
target="_blank"
>
<ArtifactAction
icon={DownloadIcon}
label={t.common.download}
tooltip={t.common.download}
/>
>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16 9V14C16 15.1046 15.1046 16 14 16H4C2.89543 16 2 15.1046 2 14V9"
stroke="#666666"
stroke-linecap="round"
/>
<path
d="M9 2V13M9 13L5 9M9 13L13 9"
stroke="#666666"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</ArtifactAction>
</a>
)}
{/* 全屏按钮 */}
<ArtifactAction
label={
fullscreen ? t.common.closeFullScreen : t.common.fullScreen
}
onClick={handleFullscreenToggle}
tooltip={
fullscreen ? t.common.closeFullScreen : t.common.fullScreen
}
>
{fullscreen ? (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 2V4C6 5.10457 5.10457 6 4 6H2"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M6 16V14C6 12.8954 5.10457 12 4 12H2"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12 2V4C12 5.10457 12.8954 6 14 6H16"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M12 16V14C12 12.8954 12.8954 12 14 12H16"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
) : (
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.75 15.5H4.5C3.39543 15.5 2.5 14.6046 2.5 13.5V12.25M2.5 5.75V4.5C2.5 3.39543 3.39543 2.5 4.5 2.5H5.75M12.25 2.5H13.5C14.6046 2.5 15.5 3.39543 15.5 4.5V5.75M15.5 12.25V13.5C15.5 14.6046 14.6046 15.5 13.5 15.5H12.25"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
)}
</ArtifactAction>
{/* 关闭按钮 */}
<ArtifactAction
icon={XIcon}
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"
>
<path
d="M4 14L14 4M4 4L14 14"
stroke="#666666"
stroke-linecap="round"
/>
</svg>
</ArtifactAction>
</ArtifactActions>
</div>
</ArtifactHeader>

View File

@ -21,6 +21,9 @@ export interface ArtifactsContextType {
open: boolean;
autoOpen: boolean;
setOpen: (open: boolean) => void;
fullscreen: boolean;
setFullscreen: (fullscreen: boolean) => void;
}
const ArtifactsContext = createContext<ArtifactsContextType | undefined>(
@ -39,6 +42,7 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true",
);
const [autoOpen, setAutoOpen] = useState(true);
const [fullscreen, setFullscreen] = useState(false);
const { setOpen: setSidebarOpen } = useSidebar();
const select = useCallback(
@ -78,6 +82,9 @@ export function ArtifactsProvider({ children }: ArtifactsProviderProps) {
selectedArtifact,
select,
deselect,
fullscreen,
setFullscreen,
};
return (

View File

@ -21,6 +21,7 @@ import {
} from "../artifacts";
import { useThread } from "../messages/context";
const FULLSCREEN_MODE = { chat: 0, artifacts: 100 };
const CLOSE_MODE = { chat: 100, artifacts: 0 };
const OPEN_MODE = { chat: 50, artifacts: 50 };
@ -40,6 +41,7 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
select: selectArtifact,
deselect,
selectedArtifact,
fullscreen,
} = useArtifacts();
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
@ -88,13 +90,15 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
useEffect(() => {
if (layoutRef.current) {
if (artifactPanelOpen) {
if (fullscreen) {
layoutRef.current.setLayout(FULLSCREEN_MODE);
} else if (artifactPanelOpen) {
layoutRef.current.setLayout(OPEN_MODE);
} else {
layoutRef.current.setLayout(CLOSE_MODE);
}
}
}, [artifactPanelOpen]);
}, [artifactPanelOpen, fullscreen]);
return (
<ResizablePanelGroup
@ -103,7 +107,10 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
groupRef={layoutRef}
>
<ResizablePanel
className="relative overflow-hidden rounded-t-[20px]"
className={cn(
"relative overflow-hidden rounded-t-[20px] transition-opacity duration-300 ease-in-out",
fullscreen && "pointer-events-none opacity-0",
)}
defaultSize={100}
id="chat"
>
@ -111,8 +118,9 @@ const ChatBox: React.FC<{ children: React.ReactNode; threadId: string }> = ({
</ResizablePanel>
<ResizableHandle
className={cn(
"opacity-33 hover:opacity-100",
"opacity-33 transition-opacity duration-300 hover:opacity-100",
!artifactPanelOpen && "pointer-events-none opacity-0",
fullscreen && "pointer-events-none opacity-0",
)}
/>
<ResizablePanel

View File

@ -15,6 +15,8 @@ export interface Translations {
share: string;
openInNewWindow: string;
close: string;
fullScreen: string;
closeFullScreen: string;
more: string;
search: string;
download: string;

View File

@ -26,6 +26,8 @@ export const zhCN: Translations = {
share: "分享",
openInNewWindow: "在新窗口打开",
close: "关闭",
fullScreen: "全屏",
closeFullScreen: "关闭全屏",
more: "更多",
search: "搜索",
download: "下载",