UNPKG

cranberrry

Version:

AI Agentic UI Framework For Frontend

500 lines (491 loc) 14.9 kB
import { nanoid } from 'nanoid'; import mitt from 'mitt'; import { createContext, useContext, useState, useRef, useEffect, useCallback } from 'react'; import { jsx } from 'react/jsx-runtime'; // cranberrry - AI Agentic UI Framework For Frontend // https://github.com/Ritvyk/cranberrry // core/agentSlice.ts var initialState = { agents: [], tasks: [] }; function CBAgentReducer(state = initialState, action) { switch (action.type) { case "ADD_AGENT": return { ...state, agents: [...state.agents, action.payload] }; case "UPDATE_AGENT": return { ...state, agents: state.agents.map( (agent) => agent.id === action.payload.id ? { ...agent, ...action.payload } : agent ) }; case "ADD_TASK": return { ...state, tasks: [...state.tasks, action.payload] }; case "UPDATE_TASK": return { ...state, tasks: state.tasks.map( (task) => task.id === action.payload.id ? { ...task, ...action.payload } : task ) }; case "REMOVE_TASK": return { ...state, tasks: state.tasks.filter((task) => task.id !== action.payload.id) }; case "ADD_MESSAGE_BLOCK": return { ...state, tasks: state.tasks.map( (task) => task.id === action.payload.taskId ? { ...task, messageBlocks: [...task.messageBlocks || [], action.payload.block] } : task ) }; default: return state; } } var addAgent = (agent) => ({ type: "ADD_AGENT", payload: agent }); var updateAgent = (agent) => ({ type: "UPDATE_AGENT", payload: agent }); var addTask = (task) => ({ type: "ADD_TASK", payload: task }); var updateTask = (task) => ({ type: "UPDATE_TASK", payload: task }); var removeTask = (id) => ({ type: "REMOVE_TASK", payload: { id } }); var addMessageBlock = (taskId, block) => ({ type: "ADD_MESSAGE_BLOCK", payload: { taskId, block } }); var getTasksForAgent = (state, agentId) => { return state.tasks.filter((task) => task.agentId === agentId); }; var globalEmitter = typeof window !== "undefined" ? window.__cranberrryEmitter || (window.__cranberrryEmitter = mitt()) : mitt(); function createAgentSupervisor({ callbacks, tagConfigs, emitter = globalEmitter }) { let status = "idle"; let buffer = ""; let currentTaskId = ""; let openTagPositions = []; function getStatus() { return status; } function startTask(taskId) { status = "ongoing"; buffer = ""; openTagPositions = []; currentTaskId = taskId; } function parseChunk(chunk) { if (status !== "ongoing") return; buffer += chunk; let foundBlock = false; for (const tagConfig of tagConfigs) { const { tag, processor, component } = tagConfig; const openingTagRegex = new RegExp(`<${tag}>`, "g"); let match; while ((match = openingTagRegex.exec(buffer)) !== null) { if (!openTagPositions.some((t) => t.tag === tag && t.index === match.index)) { openTagPositions.push({ tag, index: match.index }); callbacks.onBlockFound?.(tag); } } const blockRegex = new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`, "g"); while ((match = blockRegex.exec(buffer)) !== null) { let rawContent = match[1]; let content = rawContent; if (processor === "JSON") { try { content = JSON.parse(rawContent); } catch { content = rawContent; } } const block = { id: nanoid(), tag, content, createdAt: Date.now(), component, taskId: currentTaskId }; callbacks.onBlockProcessed?.(block); emitter.emit("message", { taskId: currentTaskId, block }); buffer = buffer.slice(0, match.index) + buffer.slice(match.index + match[0].length); openTagPositions = openTagPositions.filter((t) => !(t.tag === tag && t.index === match.index)); blockRegex.lastIndex = 0; foundBlock = true; } } if (foundBlock) { parseChunk(""); } else if (chunk === "" && buffer === "" && status === "ongoing") { status = "completed"; callbacks.onComplete?.(); } } function complete() { status = "completed"; callbacks.onComplete?.(); } function error(err) { status = "failed"; callbacks.onError?.(err); } function reset() { status = "idle"; buffer = ""; openTagPositions = []; } return { getStatus, startTask, parseChunk, complete, error, reset }; } // core/agentTaskManager.ts function getTask(tasks, id) { return tasks.find((task) => task.id === id); } function setTaskStatus(tasks, id, status) { return tasks.map((task) => task.id === id ? { ...task, status } : task); } // core/store.ts function createCBStore(reducer, preloadedState) { let state = preloadedState; let listeners = []; function getState() { return state; } function dispatch(action) { state = reducer(state, action); listeners.forEach((listener) => listener()); } function subscribe(listener) { listeners.push(listener); return () => { const index = listeners.indexOf(listener); if (index > -1) listeners.splice(index, 1); }; } return { getState, dispatch, subscribe }; } var CranberrryStoreContext = createContext(null); CranberrryStoreContext.displayName = "CranberrryStoreContext"; function CranberrryProvider({ store, children }) { return /* @__PURE__ */ jsx(CranberrryStoreContext.Provider, { value: store, children }); } function useCranberrryStore() { const store = useContext(CranberrryStoreContext); if (!store) { throw new Error("useCranberrryStore must be used within a CranberrryProvider"); } return store; } // hooks/useAgentDispatch.ts function useCBDispatch() { return useCranberrryStore().dispatch; } function useCBSelector(selector) { const store = useCranberrryStore(); const [selected, setSelected] = useState(() => selector(store.getState())); const latestSelected = useRef(selected); useEffect(() => { function checkForUpdates() { const newSelected = selector(store.getState()); if (newSelected !== latestSelected.current) { latestSelected.current = newSelected; setSelected(newSelected); } } const unsubscribe = store.subscribe(checkForUpdates); return unsubscribe; }, [store, selector]); return selected; } var useAgentSelector = useCBSelector; // hooks/useAgentTaskManager.ts function useCBTaskManager() { const dispatch = useCBDispatch(); const tasks = useCBSelector((state) => state.tasks); const createTask = useCallback((agentId, input, meta) => { const id = Math.random().toString(36).substr(2, 9); const now = Date.now(); dispatch( addTask({ id, agentId, input, status: "ongoing", createdAt: now, updatedAt: now, meta }) ); return id; }, [dispatch]); const updateTask2 = useCallback((id, updates) => { dispatch(updateTask({ id, ...updates })); }, [dispatch]); const removeTask2 = useCallback((id) => { dispatch(removeTask(id)); }, [dispatch]); const getTask2 = useCallback((id) => { return tasks.find((task) => task.id === id); }, [tasks]); const setTaskStatus2 = useCallback((id, status) => { dispatch(updateTask({ id, status, updatedAt: Date.now() })); }, [dispatch]); const addMessageBlockToTask = useCallback((taskId, block) => { dispatch(addMessageBlock(taskId, block)); }, [dispatch]); return { tasks, createTask, updateTask: updateTask2, removeTask: removeTask2, getTask: getTask2, setTaskStatus: setTaskStatus2, addMessageBlockToTask }; } // hooks/useAIAgent.ts function useCBAgent() { const agents = useCBSelector((state) => state.agents); const getAgentById = (id) => agents.find((agent) => agent.id === id); return { agents, getAgentById }; } var globalEmitter2 = typeof window !== "undefined" ? window.__cranberrryEmitter || (window.__cranberrryEmitter = mitt()) : mitt(); function useCBTaskSupervisor({ callbacks, tagConfigs, emitter = globalEmitter2 }) { const supervisorRef = useRef(null); const [status, setStatus] = useState("idle"); const [error, setError] = useState(null); if (!supervisorRef.current) { supervisorRef.current = createAgentSupervisor({ callbacks, tagConfigs, emitter }); } const startTask = useCallback((taskId) => { supervisorRef.current?.startTask(taskId); setStatus("ongoing"); setError(null); }, []); const parseChunk = useCallback((chunk) => { supervisorRef.current?.parseChunk(chunk); }, []); const complete = useCallback(() => { supervisorRef.current?.complete(); setStatus("completed"); }, []); const errorCallback = useCallback((err) => { supervisorRef.current?.error(err); setStatus("failed"); setError(err); }, []); const reset = useCallback(() => { supervisorRef.current?.reset(); setStatus("idle"); setError(null); }, []); return { status, startTask, parseChunk, complete, error, errorCallback, reset }; } var globalEmitter3 = typeof window !== "undefined" ? window.__cranberrryEmitter || (window.__cranberrryEmitter = mitt()) : mitt(); function useCBController({ agentId, tagConfigs, callbacks }) { const [prompt, setPrompt] = useState(""); const [errorMessage, setErrorMessage] = useState(null); const [currentTaskId, setCurrentTaskId] = useState(null); const currentTaskIdRef = useRef(null); const dispatch = useCBDispatch(); const emitter = globalEmitter3; const { tasks, createTask, updateTask: updateTask2, removeTask: removeTask2, getTask: getTask2, setTaskStatus: setTaskStatus2, addMessageBlockToTask // addMessageBlock, getMessageBlocks imported above } = useCBTaskManager(); currentTaskIdRef.current = currentTaskId; const wrappedCallbacks = { ...callbacks, onComplete: () => { if (currentTaskIdRef.current) { updateTask2(currentTaskIdRef.current, { status: "completed", updatedAt: Date.now() }); } dispatch(updateAgent({ id: agentId, isBusy: false })); callbacks.onComplete?.(); }, onError: (err) => { if (currentTaskIdRef.current) { updateTask2(currentTaskIdRef.current, { status: "failed", updatedAt: Date.now() }); } dispatch(updateAgent({ id: agentId, isBusy: false })); callbacks.onError?.(err); } }; const [messageBlocks, setMessageBlocks] = useState([]); useEffect(() => { if (currentTaskId) { setMessageBlocks(getTask2(currentTaskId)?.messageBlocks || []); } else { setMessageBlocks([]); } }, [currentTaskId, tasks]); const supervisor = useCBTaskSupervisor({ callbacks: { ...wrappedCallbacks, onBlockProcessed: (block) => { if (currentTaskIdRef.current) { addMessageBlockToTask(currentTaskIdRef.current, block); } callbacks.onBlockProcessed?.(block); }, onBlockFound: (tag) => { callbacks.onBlockFound?.(tag); } }, tagConfigs, emitter }); const { status, startTask, parseChunk, complete, error: supervisorError, errorCallback, reset } = supervisor; const isRunning = status === "ongoing"; const startAgentTask = useCallback( (newPrompt, newMeta) => { if (!newPrompt) throw new Error("Prompt is required"); setPrompt(newPrompt); setErrorMessage(null); const newTaskId = createTask(agentId, newPrompt, newMeta); setCurrentTaskId(newTaskId); currentTaskIdRef.current = newTaskId; dispatch(updateAgent({ id: agentId, isBusy: true })); startTask(newTaskId); return newTaskId; }, [startTask, createTask, tagConfigs, agentId, dispatch] ); const startExistingAgentTask = useCallback( (taskId) => { const existingTask = getTask2(taskId); if (!existingTask) { throw new Error(`Task with ID ${taskId} not found`); } if (existingTask.agentId !== agentId) { throw new Error(`Task ${taskId} does not belong to agent ${agentId}`); } if (existingTask.status === "completed") { throw new Error(`Task ${taskId} is already completed and cannot be resumed`); } if (existingTask.status === "failed") { updateTask2(taskId, { status: "ongoing", updatedAt: Date.now() }); } setPrompt(existingTask.input); setErrorMessage(null); setCurrentTaskId(taskId); currentTaskIdRef.current = taskId; dispatch(updateAgent({ id: agentId, isBusy: true })); startTask(taskId); return taskId; }, [getTask2, agentId, updateTask2, startTask, dispatch] ); const completeWithError = useCallback((err) => { setErrorMessage(err); callbacks.onError?.(err); errorCallback(err); }, [callbacks, errorCallback]); return { prompt, setPrompt, isRunning, error: errorMessage || supervisorError, startAgentTask, startExistingAgentTask, parseChunk, complete, reset, status, tasks, updateTask: updateTask2, removeTask: removeTask2, getTask: getTask2, setTaskStatus: setTaskStatus2, completeWithError, messageBlocks, emitter }; } var globalEmitter4 = typeof window !== "undefined" ? window.__cranberrryEmitter || (window.__cranberrryEmitter = mitt()) : mitt(); var CranberrryRenderer = ({ taskId }) => { const messageBlocks = useAgentSelector( (state) => state.tasks.find((t) => t.id === taskId)?.messageBlocks || [] ); const [liveBlocks, setLiveBlocks] = useState(messageBlocks); useEffect(() => { setLiveBlocks(messageBlocks); }, [messageBlocks]); useEffect(() => { function onMessage({ taskId: incomingTaskId, block }) { if (incomingTaskId === taskId) { setLiveBlocks((prev) => [...prev, block]); } } globalEmitter4.on("message", onMessage); return () => { globalEmitter4.off("message", onMessage); }; }, [taskId]); return /* @__PURE__ */ jsx("div", { className: "cranberrry-renderer-messages space-y-2", children: liveBlocks.map((block) => { const BlockComponent = block.component; return /* @__PURE__ */ jsx(BlockComponent, { ai: block.content }, block.id); }) }); }; export { CBAgentReducer, CranberrryProvider, CranberrryRenderer, addAgent, addMessageBlock, addTask, createAgentSupervisor, createCBStore, getTask, getTasksForAgent, removeTask, setTaskStatus, updateAgent, updateTask, useAgentSelector, useCBAgent, useCBController, useCBDispatch, useCBSelector, useCBTaskManager, useCBTaskSupervisor, useCranberrryStore }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map