import { useStore } from '@nanostores/react'; import classNames from 'classnames'; import { type HTMLMotionProps, motion, type Variants } from 'framer-motion'; import { memo, useCallback, useMemo, useState } from 'react'; import { toast } from 'sonner'; import type { OnChangeCallback, OnLoadCallback, OnReadyCallback, OnSaveCallback, } from '~/.client/components/editor/Editor'; import { PushToGitHubDialog } from '~/.client/components/header/connections/components/PushToGitHubDialog'; import { IconButton } from '~/.client/components/ui/IconButton'; import { PanelHeaderButton } from '~/.client/components/ui/PanelHeaderButton'; import { Slider, type SliderOptions } from '~/.client/components/ui/Slider'; import useViewport from '~/.client/hooks'; import { useProject } from '~/.client/hooks/useProject'; import { useChatHistory } from '~/.client/persistence'; import { aiState } from '~/.client/stores/ai-state'; import { type WebBuilderViewType, webBuilderStore } from '~/.client/stores/web-builder'; import { cubicEasingFn } from '~/.client/utils/easings'; import type { Page } from '~/types/actions'; import type { PageMap } from '~/types/pages'; import { renderLogger } from '~/utils/logger'; import { DiffView } from './DiffView'; import { EditorPanel } from './EditorPanel'; import { PageModifiedDropdown } from './PageModifiedDropdown'; import { Preview } from './Preview'; const viewTransition = { ease: cubicEasingFn }; const sliderOptions: SliderOptions = { left: { value: 'code', text: '可视化', }, middle: { value: 'diff', text: '差异', }, right: { value: 'preview', text: '预览', }, }; const workbenchVariants = { closed: { width: 0, transition: { duration: 0.2, ease: cubicEasingFn, }, }, open: { width: 'var(--workbench-width)', transition: { duration: 0.2, ease: cubicEasingFn, }, }, } satisfies Variants; export const WebBuilder = memo(() => { renderLogger.trace('webBuilder'); const [isPushDialogOpen, setIsPushDialogOpen] = useState(false); // const hasPreview = useStore(computed(webBuilderStore.previews, (previews) => previews.length > 0)); const showWorkbench = useStore(webBuilderStore.showWorkbench); const { showChat, chatStarted, isStreaming } = useStore(aiState); const documents = useStore(webBuilderStore.editorStore.editorDocuments); const currentPage = useStore(webBuilderStore.editorStore.selectedDocument); const currentSection = useStore(webBuilderStore.pagesStore.currentSection); const unsavedPages = useStore(webBuilderStore.editorStore.unsavedDocuments); const pages = useStore(webBuilderStore.pagesStore.pages); const selectedView = useStore(webBuilderStore.currentView); const isSmallViewport = useViewport(1024); const { saveProject } = useProject(); const { getLoadProject } = useChatHistory(); const setSelectedView = useCallback( (view: WebBuilderViewType) => { if (isStreaming && view !== 'code') { return; } webBuilderStore.currentView.set(view); }, [isStreaming], ); const exportable = useMemo(() => { return Object.keys(pages).length > 0; }, [pages]); const onEditorChange = useCallback((_, pageName, html) => { webBuilderStore.setDocumentContent(pageName, html); }, []); const onPageSelect = useCallback((pageName: string | undefined) => { webBuilderStore.setSelectedPage(pageName); }, []); const onPageSave = useCallback(() => { if (isStreaming) { return; } doPageSave(); }, [isStreaming]); const doPageSave = useCallback(() => { webBuilderStore.saveAllPages('user').catch(() => { toast.error('文件内容更新失败'); }); const currentMessageId = webBuilderStore.chatStore.currentMessageId.get(); if (currentMessageId) { saveProject(currentMessageId); } }, [saveProject]); const onPageReset = useCallback(() => { webBuilderStore.resetCurrentPage(); }, []); const onLoad = useCallback(async () => { const pages = await handleLoadProject(); const pageMap = Object.fromEntries(pages.map((page) => [page.name, page])) as PageMap; webBuilderStore.setPages(pageMap); }, []); const onReady = useCallback((editor) => { webBuilderStore.editorStore.setEditorInstance(editor); }, []); const handleSelectPage = useCallback((pageName: string) => { webBuilderStore.setSelectedPage(pageName); webBuilderStore.currentView.set('diff'); }, []); // 处理保存的数据,将其转为编辑器可直接使用的格式 const handleLoadProject = useCallback(async (): Promise => { const projectData = await getLoadProject(); // 新版本数据 if (projectData?.pages) { // html 为 pages 中 index 的 content return projectData.pages; } return []; }, [getLoadProject]); return ( chatStarted && (
{selectedView === 'code' && (
{unsavedPages.size > 0 && (
保存 )} { webBuilderStore.exportToZip(); }} >
下载代码 setIsPushDialogOpen(true)} >
推送到 GitHub
)} {selectedView === 'diff' && } { webBuilderStore.showWorkbench.set(false); }} />
{selectedView === 'diff' && !isStreaming ? :
}
{selectedView === 'preview' ? :
}
setIsPushDialogOpen(false)} onPush={async (repoName, username, token) => { try { const commitMessage = prompt('请输入提交信息:', 'Initial commit') || 'Initial commit'; await webBuilderStore.pushToGitHub(repoName, commitMessage, username, token); const repoUrl = `https://github.com/${username}/${repoName}`; return repoUrl; } catch (error) { console.error('Error pushing to GitHub:', error); toast.error('GitHub 推送失败'); throw error; } }} /> ) ); }); // View component for rendering content with motion transitions interface ViewProps extends HTMLMotionProps<'div'> { children: React.JSX.Element; } const View = memo(({ children, ...props }: ViewProps) => { return ( {children} ); });