import { Popover, Transition } from '@headlessui/react'; import { useStore } from '@nanostores/react'; import classNames from 'classnames'; import { type Change, diffLines } from 'diff'; import { type HTMLMotionProps, motion, type Variants } from 'framer-motion'; import { memo, useCallback, useMemo, useState } from 'react'; import { toast } from 'sonner'; import { PushToGitHubDialog } from '~/components/header/connections/components/PushToGitHubDialog'; import { IconButton } from '~/components/ui/IconButton'; import { PanelHeaderButton } from '~/components/ui/PanelHeaderButton'; import { Slider, type SliderOptions } from '~/components/ui/Slider'; import useViewport from '~/lib/hooks'; import { useProject } from '~/lib/hooks/useProject'; import { useChatHistory } from '~/lib/persistence'; import { aiState } from '~/lib/stores/ai-state'; import type { PageMap } from '~/lib/stores/pages'; import { type WebBuilderViewType, webBuilderStore } from '~/lib/stores/web-builder'; import type { Page } from '~/types/actions'; import { cubicEasingFn } from '~/utils/easings'; import { renderLogger } from '~/utils/logger'; import type { OnChangeCallback, OnLoadCallback, OnReadyCallback, OnSaveCallback } from '../editor/Editor'; import { DiffView } from './DiffView'; import { EditorPanel } from './EditorPanel'; 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; const PageModifiedDropdown = memo(({ onSelectPage }: { onSelectPage: (pageName: string) => void }) => { const pageHistory = useStore(webBuilderStore.pagesStore.pageHistory); const modifiedPages = Object.entries(pageHistory); const hasChanges = modifiedPages.length > 0; const [searchQuery, setSearchQuery] = useState(''); const filteredPages = useMemo(() => { return modifiedPages.filter(([pageName]) => pageName.toLowerCase().includes(searchQuery.toLowerCase())); }, [modifiedPages, searchQuery]); return (
{({ open }: { open: boolean }) => ( <> 更改页面 {hasChanges && ( {modifiedPages.length} )}
setSearchQuery(e.target.value)} className="w-full pl-8 pr-3 py-1.5 text-sm rounded-lg bg-upage-elements-background-depth-1 border border-upage-elements-borderColor focus:outline-none focus:ring-2 focus:ring-blue-500/50" />
{filteredPages.length > 0 ? ( filteredPages.map(([pageName, history]) => { return ( ); }) ) : (

{searchQuery ? '没有匹配的页面' : '没有修改的页面'}

{searchQuery ? '尝试其他搜索' : '更改将在此处显示'}

)}
)}
); }); 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 onAutoPageSave = useCallback(() => { if (isStreaming) { return; } doPageSave(); }, [isStreaming]); const doPageSave = useCallback(() => { webBuilderStore.saveAllPages().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} ); });