import { diffLines } from 'diff'; import { atom, computed, type MapStore, map, type WritableAtom } from 'nanostores'; import { type EditorBridge, type EventPayload, editorBridge } from '~/lib/bridge'; import type { Page, PageHistory, Section } from '~/types/actions'; import { computePageModifications, diffPages } from '~/utils/diff'; import { isValidContent } from '~/utils/html-parse'; import { createScopedLogger } from '~/utils/logger'; const logger = createScopedLogger('PagesStore'); export type PageSection = Section & { validRootDomId?: boolean; }; export type PageMap = Record; type ActiveSection = WritableAtom; type ActivePage = WritableAtom; export type SectionMap = Record; /** * 保存与 AI 交互的页面数据, AI 生成的页面数据会保存在此处。 * 当用户在两个消息之间修改 editor 时,也会讲修改的页面和内容保存在此处。 */ export class PagesStore { private readonly editorBridge: Promise = editorBridge; /** * 跟踪页面数量 */ private size = 0; /** * @note 跟踪所有自上次用户消息以来被修改的文件及其原始内容,以便模型感知这些更改。 * 当用户发送另一条消息且所有更改都需要提交时,需要重置。 */ private modifiedPages: Map = import.meta.hot?.data?.modifiedPages ?? new Map(); /** * 跟踪已删除的页面,防止它们在重新加载时重新出现 */ private deletedPages: Set = import.meta.hot?.data?.deletedPages ?? new Set(); /** * 页面映射,与 AI 做交互,基于 artifacts 数据解析而来。 * 因此,此数据表示与数据库通信的底层数据,未保存的数据将不会在此处体现。 * 如果在编辑器中确定保存了数据,则需要实时同步进 #modifiedPages 中。 */ pages: MapStore = import.meta.hot?.data?.pages ?? map({}); /** * 页面历史记录,用于 diff 视图。 * 每次页面保存时,会保存上一次的页面内容。 */ pageHistory: MapStore> = import.meta.hot?.data?.pageHistory ?? map({}); activePage: ActivePage = import.meta.hot?.data?.activePage ?? atom(); currentPage = computed([this.pages, this.activePage], (pages, activePage) => { if (!activePage) { return undefined; } return pages[activePage]; }); /** * 基于 action 的 section 映射,作为与 AI 交互的底层数据,基于 actions 数据解析而来。 */ sections: MapStore = import.meta.hot?.data?.sections ?? map({}); /** * 当前活跃的 section。 */ activeSection: ActiveSection = import.meta.hot?.data?.activeSection ?? atom(); currentSection = computed([this.sections, this.activeSection], (sections, activeSection) => { if (!activeSection) { return undefined; } return sections[activeSection]; }); get pagesCount() { return this.size; } constructor() { // Load deleted paths from localStorage if available try { if (typeof localStorage !== 'undefined') { const deletedPagesJson = localStorage.getItem('upage-deleted-pages'); if (deletedPagesJson) { const deletedPages = JSON.parse(deletedPagesJson); if (Array.isArray(deletedPages)) { deletedPages.forEach((path) => this.deletedPages.add(path)); } } } } catch (error) { logger.error('Failed to load deleted paths from localStorage', error); } if (import.meta.hot && import.meta.hot.data) { // Persist our state across hot reloads import.meta.hot.data.pages = this.pages; import.meta.hot.data.modifiedPages = this.modifiedPages; import.meta.hot.data.deletedPages = this.deletedPages; import.meta.hot.data.sections = this.sections; import.meta.hot.data.pageHistory = this.pageHistory; } this.#init(); this.setupCoordination(); } private setupCoordination() { this.sections.listen(() => { const currentPage = this.activePage.get(); if (currentPage && this.currentSection.get() === undefined) { const pageActions = this.getPage(currentPage)?.actionIds; if (pageActions) { this.setActiveSection(pageActions[pageActions.length - 1]); } } }); } getPage(pageName: string) { return this.pages.get()[pageName]; } getPageModifications() { return computePageModifications(this.pages.get(), this.modifiedPages); } getModifiedPages() { let modifiedPages: { [pageName: string]: Page } | undefined = undefined; for (const [pageName, originalContent] of this.modifiedPages) { const page = this.pages.get()[pageName]; if (!page) { continue; } if (page.content === originalContent) { continue; } if (!modifiedPages) { modifiedPages = {}; } modifiedPages[pageName] = page; } return modifiedPages; } resetPageModifications() { this.modifiedPages.clear(); } async savePage(pageName: string, content: string) { const page = this.getPage(pageName); if (!page) { return false; } // 保存上一次的页面内容 this.savePageHistory(pageName, content); try { this.pages.setKey(pageName, { ...page, content }); logger.info('Page updated'); } catch (error) { logger.error('Failed to update page content\n\n', error); throw error; } } async savePageHistory(pageName: string, newContent: string) { const page = this.getPage(pageName); if (!page) { return; } const pageHistory = this.pageHistory.get()[pageName]; // 如果不存在历史记录,则创建一个新的历史记录 const normalizedCurrentContent = newContent?.replace(/\r\n/g, '\n').trim(); const originalContent = pageHistory?.originalContent || page.content!; if (!originalContent) { return; } const normalizedOriginalContent = (pageHistory?.originalContent || page.content!).replace(/\r\n/g, '\n').trim(); if (!pageHistory) { if (normalizedCurrentContent !== normalizedOriginalContent) { const newChanges = diffLines(page.content!, newContent); const newHistory: PageHistory = { originalContent: page.content!, lastModified: Date.now(), changes: newChanges, versions: [ { timestamp: Date.now(), content: newContent, }, ], changeSource: 'auto-save', }; this.pageHistory.setKey(pageName, newHistory); } return; } // 如果存在历史记录,则检查自上次版本以来是否有实际变化 const lastVersion = pageHistory.versions[pageHistory.versions.length - 1]; const normalizedLastContent = lastVersion?.content.replace(/\r\n/g, '\n').trim(); if (normalizedCurrentContent === normalizedLastContent) { return; } const unifiedDiff = diffPages(pageName, pageHistory.originalContent, newContent); if (!unifiedDiff) { return; } const newChanges = diffLines(pageHistory.originalContent, newContent); // 检查是否有显著变化 const hasSignificantChanges = newChanges.some( (change) => (change.added || change.removed) && change.value.trim().length > 0, ); if (!hasSignificantChanges) { return; } const newHistory: PageHistory = { originalContent: pageHistory.originalContent, lastModified: Date.now(), changes: [...pageHistory.changes, ...newChanges].slice(-100), // Limitar histórico de mudanças versions: [ ...pageHistory.versions, { timestamp: Date.now(), content: newContent, }, ].slice(-10), // 只保留最近的 10 个版本 changeSource: 'auto-save', }; this.pageHistory.setKey(pageName, newHistory); } async #init() { const grapesBridge = await this.editorBridge; this.#cleanupDeletedPages(); grapesBridge.watch(({ type, payload }) => this.#processGrapesBridgeEvent(type, payload)); } /** * Removes any deleted files/folders from the store */ #cleanupDeletedPages() { if (this.deletedPages.size === 0) { return; } const currentPages = this.pages.get(); for (const deletedPageName of this.deletedPages) { if (currentPages[deletedPageName]) { this.pages.setKey(deletedPageName, undefined); this.size--; } for (const [path] of Object.entries(currentPages)) { if (path.startsWith(deletedPageName + '/')) { this.pages.setKey(path, undefined); this.size--; if (this.modifiedPages.has(path)) { this.modifiedPages.delete(path); } } } } } #processGrapesBridgeEvent(type: string, payload: EventPayload) { const { pageName } = payload; // Skip processing if this page was explicitly deleted if (this.deletedPages.has(pageName)) { return; } switch (type) { case 'add_page': { const { title: pageTitle, actionIds = [] } = payload; this.pages.setKey(pageName, { name: pageName, title: pageTitle, content: '', actionIds, }); this.size++; logger.info(`Page created: ${pageName}`); break; } case 'upsert_page': { const { title: pageTitle, actionIds = [] } = payload; this.pages.setKey(pageName, { name: pageName, title: pageTitle, actionIds, }); break; } case 'remove_page': { this.deletedPages.add(pageName); this.pages.setKey(pageName, undefined); this.size--; if (this.modifiedPages.has(pageName)) { this.modifiedPages.delete(pageName); } this.#persistDeletedPages(); logger.info(`Page deleted: ${pageName}`); break; } case 'update_section': { const { id, section } = payload; this.sections.setKey(id, { id, type: 'section', ...section }); break; } } } async createPage(pageName: string, pageTitle: string) { await this.editorBridge.then((grapesBridge) => grapesBridge.createPage(pageName, { title: pageTitle })); return true; } async deletePage(pageName: string) { await this.editorBridge.then((grapesBridge) => grapesBridge.removePage(pageName)); return true; } // method to persist deleted paths to localStorage #persistDeletedPages() { try { if (typeof localStorage !== 'undefined') { localStorage.setItem('upage-deleted-pages', JSON.stringify([...this.deletedPages])); } } catch (error) { logger.error('Failed to persist deleted paths to localStorage', error); } } updateSection(actionId: string, sectionContent: string) { const sections = this.sections.get(); const sectionState = sections[actionId]; if (!sectionState) { return; } const oldContent = sectionState.content; const contentChanged = oldContent !== sectionContent; if (contentChanged) { this.sections.setKey(actionId, { ...sectionState, content: sectionContent }); } this.updateSectionRootDomId(actionId, sectionState, sectionContent); } private updateSectionRootDomId(actionId: string, section: PageSection, sectionContent: string) { if (section.validRootDomId) { return; } if (section.action === 'remove') { this.sections.setKey(actionId, { ...section, rootDomId: section.domId, validRootDomId: true }); return; } const isValid = isValidContent(sectionContent); if (!isValid) { return; } const div = document.createElement('div'); div.innerHTML = sectionContent; const rootDomId = div.firstElementChild?.id; if (!rootDomId) { return; } const oldRootDomId = section.rootDomId; if (oldRootDomId && oldRootDomId === rootDomId) { this.sections.setKey(actionId, { ...section, validRootDomId: true }); } else { this.sections.setKey(actionId, { ...section, rootDomId }); } } setActiveSection(sectionId: string | undefined) { this.activeSection.set(sectionId); } setActivePage(pageName: string | undefined) { this.activePage.set(pageName); } setPage(pageName: string, page: Page) { const oldPage = this.getPage(pageName); if (!oldPage) { return; } this.pages.setKey(pageName, { ...oldPage, ...page }); } }