From 852d6b8e0dbee716ff1fdca9fc87664a0de649ff Mon Sep 17 00:00:00 2001 From: ttd11204 Date: Thu, 11 Dec 2025 13:08:19 +0700 Subject: [PATCH 01/16] feat: Implement autosave functionality with IndexedDB integration and update scene actions for cloud state management --- package-lock.json | 7 ++ package.json | 1 + .../components/creator3d/Creator3D.tsx | 97 ++++++++++++++++++- .../components/creator3d/Creator3DHeader.tsx | 1 - .../components/creator3d/ExportDialog.tsx | 1 - .../components/creator3d/SceneActions.tsx | 8 +- .../components/creator3d/useAutosave.ts | 69 +++++++++++++ .../right-sidebar/ComponentInspector.tsx | 1 - src/providers/Providers.tsx | 2 +- 9 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 src/features/creator-3d/components/creator3d/useAutosave.ts diff --git a/package-lock.json b/package-lock.json index 4c34baecb..aaf851c55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -95,6 +95,7 @@ "file-saver": "^2.0.5", "framer-motion": "^12.23.22", "gsap": "^3.13.0", + "idb-keyval": "^6.2.2", "jszip": "^3.10.1", "jwt-decode": "^4.0.0", "lodash-es": "^4.17.21", @@ -8888,6 +8889,12 @@ "node": ">=0.10.0" } }, + "node_modules/idb-keyval": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", + "integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==", + "license": "Apache-2.0" + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", diff --git a/package.json b/package.json index 78b8f7432..5792f8371 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "file-saver": "^2.0.5", "framer-motion": "^12.23.22", "gsap": "^3.13.0", + "idb-keyval": "^6.2.2", "jszip": "^3.10.1", "jwt-decode": "^4.0.0", "lodash-es": "^4.17.21", diff --git a/src/features/creator-3d/components/creator3d/Creator3D.tsx b/src/features/creator-3d/components/creator3d/Creator3D.tsx index 0f9acf1cc..b8a1f7b77 100644 --- a/src/features/creator-3d/components/creator3d/Creator3D.tsx +++ b/src/features/creator-3d/components/creator3d/Creator3D.tsx @@ -29,6 +29,8 @@ import { ApiSuccessResponse } from '@/types/baseModel' import { Emulator } from '@/features/emulator/types/emulator.type' import { buildSceneFromAssembly } from '@/features/creator-3d/hooks/buildSceneFromAssembly' import { exportGLB } from '@/features/creator-3d/hooks/exportGlb' +import { useAutosave } from '@/features/creator-3d/components/creator3d/useAutosave' +import { del } from 'idb-keyval' type Creator3DProps = { emulatorData: ApiSuccessResponse | undefined } @@ -42,6 +44,17 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { const selectedObject = useSelectedObject() const exportAssemblyFn = useExportAssembly() const fileInputRef = useRef(null) + const { actions, activities } = useAppSelector((s) => s.workspaceTree) + const creatorState = useMemo( + () => ({ + instances, + actions, + activities + }), + [instances, actions, activities] + ) + + const autosaveKey = `creator-autosave-${workspaceId}` const [updateEmulator, { isLoading: isUpdating }] = useUpdateEmulatorMutation() const prevInstanceIds = useRef([]) @@ -120,7 +133,6 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { status: existing.status } }).unwrap() - toast.success('Lưu dữ liệu thành công.') } // console.log('Emulator creation response:', response) @@ -304,6 +316,7 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { // ================================ // 🔹 Restore Activities + Steps // ================================ + console.log('Restoring activity:', data.activities) if (Array.isArray(data.activities)) { for (const activity of data.activities) { // const fullSteps: Step[] = [] @@ -531,6 +544,64 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { [dispatch] ) + const restoreFromAutosave = (data: any) => { + if (!data) return + + // Restore instances + if (Array.isArray(data.instances)) { + dispatch(setInstances(data.instances)) + } + + // Restore actions + dispatch(clearAction()) + if (Array.isArray(data.actions)) { + for (const act of data.actions) { + dispatch(addAction(act)) + + if (Array.isArray(act.targets)) { + for (const targetId of act.targets) { + dispatch(addTargetToAction({ actionId: act.id, targetId })) + } + } + + if (act.type === 'transform_arm' && act.connectorArmTransforms) { + Object.entries(act.connectorArmTransforms).forEach(([connectorId, arms]) => { + dispatch( + updateConnectorArms({ + actionId: act.id, + connectorId, + arms: arms as Record + }) + ) + }) + } + } + } + + // Restore activities + dispatch(clearActivities()) + if (Array.isArray(data.activities)) { + for (const activity of data.activities) { + dispatch( + addActivity({ + id: activity.id, + name: activity.name, + steps: [], + difficulty: activity.difficulty ?? 'medium', + description: activity.description ?? '', + estimatedTime: activity.estimatedTime ?? 10 + }) + ) + + if (Array.isArray(activity.steps)) { + for (const step of activity.steps) { + dispatch(addStepToActivity({ activityId: activity.id, step })) + } + } + } + } + } + const handleExportGLB = async () => { const assembly = exportAssemblyFn({ title: `Assembly ${workspaceId}`, @@ -578,9 +649,22 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { [dispatch] ) + const { status: cloudState, loadPromise: autosaveLoaded } = useAutosave({ + key: autosaveKey, + data: creatorState, + onLoad: restoreFromAutosave, + onSyncToServer: async () => handleSaveAssembly(), + interval: 2000, + debounce: 5000 + }) + useEffect(() => { - handleImportAssembly(workspaceId) - }, []) + autosaveLoaded.then((saved) => { + if (!saved) { + handleImportAssembly(workspaceId) + } + }) + }, [autosaveLoaded, handleImportAssembly, workspaceId]) // trong Creator3D useEffect(() => { @@ -619,7 +703,12 @@ export default function Creator3D({ emulatorData }: Creator3DProps) { /> {/* Action Buttons */} - + {/* Object Inspector */} diff --git a/src/features/creator-3d/components/creator3d/Creator3DHeader.tsx b/src/features/creator-3d/components/creator3d/Creator3DHeader.tsx index d9bc2ef50..21d2e82e2 100644 --- a/src/features/creator-3d/components/creator3d/Creator3DHeader.tsx +++ b/src/features/creator-3d/components/creator3d/Creator3DHeader.tsx @@ -50,7 +50,6 @@ export default function Creator3DHeader() {
- diff --git a/src/features/creator-3d/components/creator3d/ExportDialog.tsx b/src/features/creator-3d/components/creator3d/ExportDialog.tsx index b3a825624..d66395cae 100644 --- a/src/features/creator-3d/components/creator3d/ExportDialog.tsx +++ b/src/features/creator-3d/components/creator3d/ExportDialog.tsx @@ -27,7 +27,6 @@ export function UpsertEmulator({ emulationId }: UpsertEmulatorProps) { const [name, setName] = useState('') const [description, setDescription] = useState('') - console.log('description in UpsertEmulator:', description) const [visibility, setVisibility] = useState<'public' | 'private'>('public') const [thumbnailBase64, setThumbnailBase64] = useState() const [thumbnailFileName, setThumbnailFileName] = useState() diff --git a/src/features/creator-3d/components/creator3d/SceneActions.tsx b/src/features/creator-3d/components/creator3d/SceneActions.tsx index 98fbeb5e9..3dc06e678 100644 --- a/src/features/creator-3d/components/creator3d/SceneActions.tsx +++ b/src/features/creator-3d/components/creator3d/SceneActions.tsx @@ -1,12 +1,14 @@ +import { IconCloudCheck, IconCloudOff, IconLoader2, IconRefresh } from '@tabler/icons-react' import { useTranslations } from 'next-intl' interface SceneActionsProps { + cloudState: 'saved' | 'saving' onSave: () => void onImportJSON?: () => void onExportGLB?: () => void } -export function SceneActions({ onSave, onImportJSON, onExportGLB }: SceneActionsProps) { +export function SceneActions({ cloudState, onSave, onImportJSON, onExportGLB }: SceneActionsProps) { const t3d = useTranslations('creator3D.main_content') const tc = useTranslations('common') return ( @@ -14,8 +16,10 @@ export function SceneActions({ onSave, onImportJSON, onExportGLB }: SceneActions