feat: implement task management store with IPC integration
- Added a new task store in `src-react/stores/task.ts` to manage tasks and their statuses. - Implemented functions for creating, executing, and retrying tasks, along with handling task progress and completion. - Introduced persistence for tasks using IPC. - Created utility functions for normalizing room types and building subtasks. - Added a new CSS file for global styles in `src-react/styles.css`. - Created runtime types in `src-react/types/runtime.ts` and exported them. - Updated the main entry points for Vue and React applications to support dynamic framework loading. - Refactored chat model interfaces and utility functions into `src/shared/chat-model.ts`. - Updated TypeScript configuration to include paths for React components and types. - Enhanced Vite configuration to support both Vue and React frameworks.
This commit is contained in:
202
src-react/stores/channel.ts
Normal file
202
src-react/stores/channel.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import type { AutomationScript } from '@lib/script-types';
|
||||
import { scriptApi } from '@lib/script-api';
|
||||
import { resolveChannel } from '@constant/channel';
|
||||
import { CONFIG_KEYS, IPC_EVENTS } from '../lib/constants';
|
||||
import { invokeIpc } from '../lib/host-api';
|
||||
|
||||
export interface ChannelItem {
|
||||
id: string;
|
||||
channelName: string;
|
||||
channelUrl: string;
|
||||
}
|
||||
|
||||
export interface ChannelStoreState {
|
||||
initialized: boolean;
|
||||
loading: boolean;
|
||||
selectedChannels: ChannelItem[];
|
||||
availableChannels: ChannelItem[];
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
let initPromise: Promise<void> | null = null;
|
||||
let state: ChannelStoreState = {
|
||||
initialized: false,
|
||||
loading: false,
|
||||
selectedChannels: [],
|
||||
availableChannels: [],
|
||||
error: null,
|
||||
};
|
||||
|
||||
function emit(): void {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function patchState(patch: Partial<ChannelStoreState>): ChannelStoreState {
|
||||
state = { ...state, ...patch };
|
||||
emit();
|
||||
return state;
|
||||
}
|
||||
|
||||
function normalizeChannelItem(item: Partial<ChannelItem> | null | undefined): ChannelItem | null {
|
||||
const channelUrl = String(item?.channelUrl ?? '').trim();
|
||||
if (!channelUrl) return null;
|
||||
|
||||
const channelName = String(item?.channelName ?? channelUrl).trim() || channelUrl;
|
||||
const id = String(item?.id ?? channelUrl).trim() || channelUrl;
|
||||
|
||||
return {
|
||||
id,
|
||||
channelName,
|
||||
channelUrl,
|
||||
};
|
||||
}
|
||||
|
||||
function dedupeChannels(items: Array<Partial<ChannelItem> | null | undefined>): ChannelItem[] {
|
||||
const channelMap = new Map<string, ChannelItem>();
|
||||
|
||||
for (const item of items) {
|
||||
const normalized = normalizeChannelItem(item);
|
||||
if (!normalized || channelMap.has(normalized.channelUrl)) continue;
|
||||
channelMap.set(normalized.channelUrl, normalized);
|
||||
}
|
||||
|
||||
return Array.from(channelMap.values());
|
||||
}
|
||||
|
||||
function mapScriptsToChannels(scripts: AutomationScript[]): ChannelItem[] {
|
||||
const items: ChannelItem[] = [];
|
||||
|
||||
for (const script of scripts) {
|
||||
if (!script.channel) continue;
|
||||
|
||||
const resolved = resolveChannel(script.channel);
|
||||
const channelUrl = String(resolved.url ?? '').trim();
|
||||
if (!channelUrl) continue;
|
||||
|
||||
items.push({
|
||||
id: channelUrl,
|
||||
channelName: String(resolved.name ?? channelUrl).trim() || channelUrl,
|
||||
channelUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return dedupeChannels(items);
|
||||
}
|
||||
|
||||
async function loadSelectedChannels(): Promise<ChannelItem[]> {
|
||||
const saved = await invokeIpc<ChannelItem[]>(IPC_EVENTS.GET_CONFIG, CONFIG_KEYS.SELECTED_CHANNELS);
|
||||
return Array.isArray(saved) ? dedupeChannels(saved) : [];
|
||||
}
|
||||
|
||||
async function loadAvailableChannels(): Promise<ChannelItem[]> {
|
||||
const scripts = await scriptApi.list();
|
||||
return mapScriptsToChannels(Array.isArray(scripts) ? scripts : []);
|
||||
}
|
||||
|
||||
async function hydrate(): Promise<void> {
|
||||
patchState({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const [selectedChannels, availableChannels] = await Promise.all([
|
||||
loadSelectedChannels(),
|
||||
loadAvailableChannels(),
|
||||
]);
|
||||
|
||||
patchState({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
selectedChannels,
|
||||
availableChannels,
|
||||
error: null,
|
||||
});
|
||||
} catch (error) {
|
||||
patchState({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
selectedChannels: [],
|
||||
availableChannels: [],
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAvailableChannels(): Promise<ChannelItem[]> {
|
||||
patchState({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const availableChannels = await loadAvailableChannels();
|
||||
patchState({
|
||||
loading: false,
|
||||
availableChannels,
|
||||
error: null,
|
||||
});
|
||||
return availableChannels;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
patchState({
|
||||
loading: false,
|
||||
error: message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSelectedChannels(items: ChannelItem[]): Promise<ChannelItem[]> {
|
||||
const nextItems = dedupeChannels(items);
|
||||
patchState({ selectedChannels: nextItems, error: null });
|
||||
|
||||
await invokeIpc(IPC_EVENTS.SET_CONFIG, CONFIG_KEYS.SELECTED_CHANNELS, nextItems);
|
||||
return nextItems;
|
||||
}
|
||||
|
||||
function setSelectedChannels(items: ChannelItem[]): ChannelItem[] {
|
||||
const nextItems = dedupeChannels(items);
|
||||
patchState({ selectedChannels: nextItems });
|
||||
return nextItems;
|
||||
}
|
||||
|
||||
function addSelectedChannel(item: ChannelItem): ChannelItem[] {
|
||||
return setSelectedChannels([...state.selectedChannels, item]);
|
||||
}
|
||||
|
||||
function removeSelectedChannel(id: string): ChannelItem[] {
|
||||
return setSelectedChannels(state.selectedChannels.filter((item) => item.id !== id));
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
function getSnapshot(): ChannelStoreState {
|
||||
return state;
|
||||
}
|
||||
|
||||
async function initChannelStore(): Promise<void> {
|
||||
if (!initPromise) {
|
||||
initPromise = hydrate();
|
||||
}
|
||||
|
||||
await initPromise;
|
||||
}
|
||||
|
||||
export const channelStore = {
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
getState: () => state,
|
||||
init: initChannelStore,
|
||||
loadSelectedChannels,
|
||||
refreshAvailableChannels,
|
||||
saveSelectedChannels,
|
||||
setSelectedChannels,
|
||||
addSelectedChannel,
|
||||
removeSelectedChannel,
|
||||
};
|
||||
|
||||
export function useChannelStore(): ChannelStoreState {
|
||||
return useSyncExternalStore(channelStore.subscribe, channelStore.getSnapshot, channelStore.getSnapshot);
|
||||
}
|
||||
Reference in New Issue
Block a user