feat:全屏功能
This commit is contained in:
parent
a8d1c8367f
commit
32f581cf50
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ export interface Translations {
|
|||
share: string;
|
||||
openInNewWindow: string;
|
||||
close: string;
|
||||
fullScreen: string;
|
||||
closeFullScreen: string;
|
||||
more: string;
|
||||
search: string;
|
||||
download: string;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ export const zhCN: Translations = {
|
|||
share: "分享",
|
||||
openInNewWindow: "在新窗口打开",
|
||||
close: "关闭",
|
||||
fullScreen: "全屏",
|
||||
closeFullScreen: "关闭全屏",
|
||||
more: "更多",
|
||||
search: "搜索",
|
||||
download: "下载",
|
||||
|
|
|
|||
Loading…
Reference in New Issue