Initial ecommerce standalone package
This commit is contained in:
@@ -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';
|
||||
@@ -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: [],
|
||||
}),
|
||||
}));
|
||||
@@ -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 };
|
||||
});
|
||||
},
|
||||
}));
|
||||
@@ -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),
|
||||
}));
|
||||
@@ -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,
|
||||
}),
|
||||
}));
|
||||
@@ -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: [] }),
|
||||
}));
|
||||
Reference in New Issue
Block a user