Initial commit: OmniAI Web Frontend

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:38:01 +08:00
commit bedee3ba8d
183 changed files with 94805 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
// Zustand store exports
export { useSessionStore } from './useSessionStore';
export { useProjectStore } from './useProjectStore';
export { useTaskStore } from './useTaskStore';
export { useAppStore } from './useAppStore';
// 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: [],
}),
}));
+54
View File
@@ -0,0 +1,54 @@
import { create } from 'zustand';
import type { WebProjectSummary, WebCanvasWorkflow } from '../types';
interface ProjectState {
projects: WebProjectSummary[];
projectsLoaded: boolean;
canvasWorkflow: WebCanvasWorkflow | null;
currentCanvasProjectId: string | null;
pendingDeleteProject: WebProjectSummary | null;
deleteProjectSubmitting: boolean;
}
interface ProjectActions {
setProjects: (projects: WebProjectSummary[]) => void;
setProjectsLoaded: (loaded: boolean) => void;
setCanvasWorkflow: (workflow: WebCanvasWorkflow | null) => 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: null,
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 || '您的账号已在其他设备登录(最多同时 2 台设备),此设备的登录状态已失效。',
}),
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: [] }),
}));