cranberrry
Version:
AI Agentic UI Framework For Frontend
500 lines (491 loc) • 14.9 kB
JavaScript
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