Initial ecommerce standalone package

This commit is contained in:
2026-06-10 14:06:16 +08:00
commit 3d98933e24
241 changed files with 135283 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
// Zustand store exports
export { useSessionStore } from './useSessionStore';
export { useProjectStore } from './useProjectStore';
export { useTaskStore } from './useTaskStore';
export { useAppStore } from './useAppStore';
export { useGenerationStore } from './useGenerationStore';
export type { GenerationQueueItem, QueueItemStatus } from './useGenerationStore';
// Type exports
export type { PendingAction } from './useSessionStore';
+111
View File
@@ -0,0 +1,111 @@
import { create } from 'zustand';
import type {
WebUsageSummary,
WebNotification,
WebViewKey,
WebImageWorkbenchTool,
} from '../types';
import type { ServerConnectionHealth } from '../api/serverConnection';
import type { TemplateCase } from '../features/ecommerce/ecommerceTemplates';
interface AppState {
usage: WebUsageSummary;
runtimeNotifications: WebNotification[];
serverNotifications: WebNotification[];
activeView: WebViewKey;
workspaceExpanded: boolean;
imageWorkbenchTool: WebImageWorkbenchTool;
pendingEcommerceTemplate: TemplateCase | null;
backendHealth: ServerConnectionHealth;
}
interface AppActions {
setUsage: (usage: WebUsageSummary) => void;
pushNotification: (notification: Omit<WebNotification, 'id' | 'createdAt' | 'isRead'>) => void;
setRuntimeNotifications: (notifications: WebNotification[]) => void;
setServerNotifications: (notifications: WebNotification[]) => void;
setView: (view: WebViewKey) => void;
setWorkspaceExpanded: (expanded: boolean) => void;
setImageWorkbenchTool: (tool: WebImageWorkbenchTool) => void;
setPendingEcommerceTemplate: (template: TemplateCase | null) => void;
setBackendHealth: (health: ServerConnectionHealth) => void;
markNotificationRead: (id: string, isRead?: boolean) => void;
markAllNotificationsRead: () => void;
clearAppState: () => void;
}
const emptyUsageSummary: WebUsageSummary = {
balanceCents: 0,
imageUsed: 0,
videoUsed: 0,
textUsed: 0,
source: 'preview',
};
const initialState: AppState = {
usage: emptyUsageSummary,
runtimeNotifications: [],
serverNotifications: [],
activeView: 'home',
workspaceExpanded: false,
imageWorkbenchTool: 'workbench',
pendingEcommerceTemplate: null,
backendHealth: {
state: 'checking',
baseUrl: '',
checkedAt: new Date().toISOString(),
},
};
export const useAppStore = create<AppState & AppActions>((set) => ({
...initialState,
setUsage: (usage) => set({ usage }),
pushNotification: (notification) => set((state) => ({
runtimeNotifications: [
{
...notification,
id: `notice-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
createdAt: new Date().toISOString(),
isRead: false,
},
...state.runtimeNotifications,
].slice(0, 30),
})),
setRuntimeNotifications: (notifications) => set({ runtimeNotifications: notifications }),
setServerNotifications: (notifications) => set({ serverNotifications: notifications }),
setView: (view) => set({ activeView: view }),
setWorkspaceExpanded: (expanded) => set({ workspaceExpanded: expanded }),
setImageWorkbenchTool: (tool) => set({ imageWorkbenchTool: tool }),
setPendingEcommerceTemplate: (template) => set({ pendingEcommerceTemplate: template }),
setBackendHealth: (health) => set({ backendHealth: health }),
markNotificationRead: (id, isRead = true) => set((state) => ({
serverNotifications: state.serverNotifications.map((item) =>
item.id === id ? { ...item, isRead, readAt: isRead ? new Date().toISOString() : null } : item
),
})),
markAllNotificationsRead: () => set((state) => ({
serverNotifications: state.serverNotifications.map((item) => ({
...item,
isRead: true,
readAt: new Date().toISOString(),
})),
})),
clearAppState: () => set({
...initialState,
usage: emptyUsageSummary,
runtimeNotifications: [],
serverNotifications: [],
}),
}));
+121
View File
@@ -0,0 +1,121 @@
import { create } from "zustand";
import type { WebGenerationPreviewTask } from "../types";
export type QueueItemStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
export interface GenerationQueueItem {
id: string;
taskId?: string;
title: string;
type: "image" | "video" | "agent" | "digital-human" | "character-mix" | "ecommerce-video";
status: QueueItemStatus;
progress: number;
prompt: string;
createdAt: number;
sourceView: string; // which page created this: "ecommerce", "workbench", "canvas", "agent"
resultUrl?: string | null;
error?: string | null;
params?: Record<string, unknown>;
}
interface PersistedQueueSnapshot {
version: 1;
items: GenerationQueueItem[];
savedAt: number;
}
const STORAGE_KEY = "omniai:generation-queue";
const MAX_ITEMS = 80;
const STALE_MS = 2 * 60 * 60 * 1000; // 2 hours
function loadPersistedQueue(): GenerationQueueItem[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const snapshot = JSON.parse(raw) as PersistedQueueSnapshot;
if (Date.now() - (snapshot.savedAt || 0) > STALE_MS) {
localStorage.removeItem(STORAGE_KEY);
return [];
}
return snapshot.items.filter(
(item) => item.status === "pending" || item.status === "running",
);
} catch {
return [];
}
}
function persistQueue(items: GenerationQueueItem[]): void {
try {
const snapshot: PersistedQueueSnapshot = { version: 1, items, savedAt: Date.now() };
localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
} catch { /* quota exceeded */ }
}
interface GenerationStoreState {
queue: GenerationQueueItem[];
addTask: (item: GenerationQueueItem) => void;
updateTask: (id: string, patch: Partial<GenerationQueueItem>) => void;
removeTask: (id: string) => void;
getRunningTasks: () => GenerationQueueItem[];
getPendingTasks: () => GenerationQueueItem[];
getTasksByView: (sourceView: string) => GenerationQueueItem[];
clearTerminal: () => void;
}
function hashUserId(): string {
try {
const raw = localStorage.getItem("omniai-web-session");
if (!raw) return "anon";
const parsed = JSON.parse(raw) as { user?: { id?: number | string } };
return String(parsed?.user?.id || "anon");
} catch {
return "anon";
}
}
const initialQueue = loadPersistedQueue();
export const useGenerationStore = create<GenerationStoreState>((set, get) => ({
queue: initialQueue,
addTask: (item) => {
set((state) => {
const next = [item, ...state.queue].slice(0, MAX_ITEMS);
persistQueue(next.filter((i) => i.status === "pending" || i.status === "running"));
return { queue: next };
});
},
updateTask: (id, patch) => {
set((state) => {
const next = state.queue.map((item) =>
item.id === id ? { ...item, ...patch } : item,
);
persistQueue(next.filter((i) => i.status === "pending" || i.status === "running"));
return { queue: next };
});
},
removeTask: (id) => {
set((state) => {
const next = state.queue.filter((item) => item.id !== id);
persistQueue(next.filter((i) => i.status === "pending" || i.status === "running"));
return { queue: next };
});
},
getRunningTasks: () => get().queue.filter((i) => i.status === "running" || i.status === "pending"),
getPendingTasks: () => get().queue.filter((i) => i.status === "pending"),
getTasksByView: (sourceView) => get().queue.filter((i) => i.sourceView === sourceView),
clearTerminal: () => {
set((state) => {
const next = state.queue.filter(
(i) => i.status === "pending" || i.status === "running",
);
persistQueue(next);
return { queue: next };
});
},
}));
+55
View File
@@ -0,0 +1,55 @@
import { create } from 'zustand';
import type { WebProjectSummary, WebCanvasWorkflow } from '../types';
import { createBlankWorkflow } from '../data/workflows';
interface ProjectState {
projects: WebProjectSummary[];
projectsLoaded: boolean;
canvasWorkflow: WebCanvasWorkflow;
currentCanvasProjectId: string | null;
pendingDeleteProject: WebProjectSummary | null;
deleteProjectSubmitting: boolean;
}
interface ProjectActions {
setProjects: (projects: WebProjectSummary[]) => void;
setProjectsLoaded: (loaded: boolean) => void;
setCanvasWorkflow: (workflow: WebCanvasWorkflow) => void;
setCurrentCanvasProjectId: (id: string | null) => void;
openDeleteProject: (project: WebProjectSummary) => void;
closeDeleteProject: () => void;
setDeleteProjectSubmitting: (submitting: boolean) => void;
clearProjectState: () => void;
}
const initialState: ProjectState = {
projects: [],
projectsLoaded: false,
canvasWorkflow: createBlankWorkflow(),
currentCanvasProjectId: null,
pendingDeleteProject: null,
deleteProjectSubmitting: false,
};
export const useProjectStore = create<ProjectState & ProjectActions>((set) => ({
...initialState,
setProjects: (projects) => set({ projects }),
setProjectsLoaded: (loaded) => set({ projectsLoaded: loaded }),
setCanvasWorkflow: (workflow) => set({ canvasWorkflow: workflow }),
setCurrentCanvasProjectId: (id) => set({ currentCanvasProjectId: id }),
openDeleteProject: (project) => set({ pendingDeleteProject: project }),
closeDeleteProject: () => set({
pendingDeleteProject: null,
deleteProjectSubmitting: false,
}),
setDeleteProjectSubmitting: (submitting) => set({ deleteProjectSubmitting: submitting }),
clearProjectState: () => set(initialState),
}));
+68
View File
@@ -0,0 +1,68 @@
import { create } from 'zustand';
import type { WebCanvasWorkflow, WebUserSession } from '../types';
import type { CreatePreviewTaskInput } from '../api/webGenerationGateway';
export interface PendingAction {
kind: 'project' | 'task';
label: string;
description: string;
workflow?: WebCanvasWorkflow;
input?: CreatePreviewTaskInput;
}
interface SessionState {
session: WebUserSession | null;
loginPromptOpen: boolean;
pendingAction: PendingAction | null;
sessionReplacedOpen: boolean;
sessionReplacedMessage: string;
}
interface SessionActions {
setSession: (session: WebUserSession | null) => void;
openLoginPrompt: (action?: PendingAction) => void;
closeLoginPrompt: () => void;
setPendingAction: (action: PendingAction | null) => void;
showSessionReplaced: (message?: string) => void;
hideSessionReplaced: () => void;
clearSession: () => void;
}
const initialState: SessionState = {
session: null,
loginPromptOpen: false,
pendingAction: null,
sessionReplacedOpen: false,
sessionReplacedMessage: '当前账号已在其他设备登录,此设备的登录状态已失效。',
};
export const useSessionStore = create<SessionState & SessionActions>((set) => ({
...initialState,
setSession: (session) => set({ session }),
openLoginPrompt: (action) => set({
loginPromptOpen: true,
pendingAction: action || null,
}),
closeLoginPrompt: () => set({
loginPromptOpen: false,
pendingAction: null,
}),
setPendingAction: (action) => set({ pendingAction: action }),
showSessionReplaced: (message) => set({
sessionReplacedOpen: true,
sessionReplacedMessage: message || '当前账号已在其他设备登录,此设备的登录状态已失效。',
}),
hideSessionReplaced: () => set({ sessionReplacedOpen: false }),
clearSession: () => set({
session: null,
loginPromptOpen: false,
pendingAction: null,
}),
}));
+36
View File
@@ -0,0 +1,36 @@
import { create } from 'zustand';
import type { WebGenerationPreviewTask } from '../types';
interface TaskState {
tasks: WebGenerationPreviewTask[];
}
interface TaskActions {
setTasks: (tasks: WebGenerationPreviewTask[]) => void;
appendTask: (task: WebGenerationPreviewTask) => void;
mergeServerTasks: (serverTasks: WebGenerationPreviewTask[]) => void;
clearTasks: () => void;
}
const initialState: TaskState = {
tasks: [],
};
export const useTaskStore = create<TaskState & TaskActions>((set) => ({
...initialState,
setTasks: (tasks) => set({ tasks }),
appendTask: (task) => set((state) => ({
tasks: [task, ...state.tasks],
})),
mergeServerTasks: (serverTasks) => set((state) => {
const serverIds = new Set(serverTasks.map((task) => task.id));
return {
tasks: [...serverTasks, ...state.tasks.filter((task) => !serverIds.has(task.id))].slice(0, 80),
};
}),
clearTasks: () => set({ tasks: [] }),
}));