cranberrry
Version:
AI Agentic UI Framework For Frontend
732 lines (705 loc) • 22.8 kB
text/typescript
import { Emitter } from 'mitt';
import * as react$1 from 'react';
import react__default, { ReactNode } from 'react';
import * as react_jsx_runtime from 'react/jsx-runtime';
type CBListener = () => void;
interface CBStore {
getState: () => CBState;
dispatch: (action: CBAction) => void;
subscribe: (listener: CBListener) => () => void;
}
type CBAgentID = string;
interface CBAgent {
id: CBAgentID;
name: string;
face?: string;
designation?: string;
description?: string;
introduction?: string;
isActive: boolean;
isAvailable: boolean;
isBusy: boolean;
}
type CBTaskStatus = 'idle' | 'ongoing' | 'completed' | 'failed';
type CBBlockProcessor = 'TEXT' | 'JSON';
interface CBTagConfig {
tag: string;
processor: CBBlockProcessor;
component: React.ComponentType<{
ai: any;
}>;
}
interface CBMessageBlock {
id: string;
tag: string;
taskId: string;
content: any;
createdAt: number;
component: React.ComponentType<{
ai: any;
}>;
}
interface CBTask {
id: string;
agentId: CBAgentID;
input: string;
status: CBTaskStatus;
createdAt: number;
updatedAt: number;
meta?: Record<string, any>;
response?: string;
messageBlocks?: CBMessageBlock[];
}
interface CBState {
agents: CBAgent[];
tasks: CBTask[];
}
type CBAction = {
type: 'ADD_AGENT';
payload: CBAgent;
} | {
type: 'UPDATE_AGENT';
payload: Partial<CBAgent> & {
id: CBAgentID;
};
} | {
type: 'ADD_TASK';
payload: CBTask;
} | {
type: 'UPDATE_TASK';
payload: Partial<CBTask> & {
id: string;
};
} | {
type: 'REMOVE_TASK';
payload: {
id: string;
};
} | {
type: 'ADD_MESSAGE_BLOCK';
payload: {
taskId: string;
block: CBMessageBlock;
};
};
type CBBlockCallback<T = any> = (block: T) => void;
type CBTaskCallbacks = {
[blockTag: string]: CBBlockCallback<any>;
} & {
onComplete?: () => void;
onError?: (error: string) => void;
};
type CBStartTaskParams = {
agentId: CBAgentID;
input: string;
callbacks: CBTaskCallbacks;
meta?: Record<string, any>;
};
declare function CBAgentReducer$1(state: CBState | undefined, action: CBAction): CBState;
declare const addAgent$1: (agent: CBAgent) => CBAction;
declare const updateAgent$1: (agent: Partial<CBAgent> & {
id: string;
}) => CBAction;
declare const addTask$1: (task: CBTask) => CBAction;
declare const updateTask$1: (task: Partial<CBTask> & {
id: string;
}) => CBAction;
declare const removeTask$1: (id: string) => CBAction;
declare const addMessageBlock$1: (taskId: string, block: CBMessageBlock) => CBAction;
declare const getTasksForAgent$1: (state: CBState, agentId: string) => CBTask[];
type CBBlockSupervisorCallbacks = {
onBlockFound?: (tag: string) => void;
onBlockProcessed?: (block: CBMessageBlock) => void;
onComplete?: () => void;
onError?: (err: string) => void;
};
declare function createAgentSupervisor$1({ callbacks, tagConfigs, emitter, }: {
callbacks: CBBlockSupervisorCallbacks;
tagConfigs: CBTagConfig[];
emitter?: Emitter<any>;
}): {
getStatus: () => CBTaskStatus;
startTask: (taskId: string) => void;
parseChunk: (chunk: string) => void;
complete: () => void;
error: (err: string) => void;
reset: () => void;
};
declare function getTask$1(tasks: CBTask[], id: string): CBTask | undefined;
declare function setTaskStatus$1(tasks: CBTask[], id: string, status: CBTaskStatus): CBTask[];
declare function createCBStore$1(reducer: (state: CBState, action: CBAction) => CBState, preloadedState: CBState): CBStore;
declare function useCBTaskManager$1(): {
tasks: CBTask[];
createTask: (agentId: string, input: string, meta?: Record<string, any>) => string;
updateTask: (id: string, updates: Partial<CBTask>) => void;
removeTask: (id: string) => void;
getTask: (id: string) => CBTask | undefined;
setTaskStatus: (id: string, status: CBTaskStatus) => void;
addMessageBlockToTask: (taskId: string, block: CBMessageBlock) => void;
};
var nanoid = require('nanoid');
var mitt = require('mitt');
var react = require('react');
var jsxRuntime = require('react/jsx-runtime');
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var mitt__default = /*#__PURE__*/_interopDefault(mitt);
// 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__default.default()) : mitt__default.default();
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.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 = react.createContext(null);
CranberrryStoreContext.displayName = "CranberrryStoreContext";
function CranberrryProvider$1({
store,
children
}) {
return /* @__PURE__ */ jsxRuntime.jsx(CranberrryStoreContext.Provider, { value: store, children });
}
function useCranberrryStore$1() {
const store = react.useContext(CranberrryStoreContext);
if (!store) {
throw new Error("useCranberrryStore must be used within a CranberrryProvider");
}
return store;
}
// hooks/useAgentDispatch.ts
function useCBDispatch$1() {
return useCranberrryStore$1().dispatch;
}
function useCBSelector$1(selector) {
const store = useCranberrryStore$1();
const [selected, setSelected] = react.useState(() => selector(store.getState()));
const latestSelected = react.useRef(selected);
react.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$1 = useCBSelector$1;
// hooks/useAgentTaskManager.ts
function useCBTaskManager() {
const dispatch = useCBDispatch$1();
const tasks = useCBSelector$1((state) => state.tasks);
const createTask = react.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 = react.useCallback((id, updates) => {
dispatch(updateTask({ id, ...updates }));
}, [dispatch]);
const removeTask2 = react.useCallback((id) => {
dispatch(removeTask(id));
}, [dispatch]);
const getTask2 = react.useCallback((id) => {
return tasks.find((task) => task.id === id);
}, [tasks]);
const setTaskStatus2 = react.useCallback((id, status) => {
dispatch(updateTask({ id, status, updatedAt: Date.now() }));
}, [dispatch]);
const addMessageBlockToTask = react.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$1() {
const agents = useCBSelector$1((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__default.default()) : mitt__default.default();
function useCBTaskSupervisor$1({
callbacks,
tagConfigs,
emitter = globalEmitter2
}) {
const supervisorRef = react.useRef(null);
const [status, setStatus] = react.useState("idle");
const [error, setError] = react.useState(null);
if (!supervisorRef.current) {
supervisorRef.current = createAgentSupervisor({ callbacks, tagConfigs, emitter });
}
const startTask = react.useCallback((taskId) => {
supervisorRef.current?.startTask(taskId);
setStatus("ongoing");
setError(null);
}, []);
const parseChunk = react.useCallback((chunk) => {
supervisorRef.current?.parseChunk(chunk);
}, []);
const complete = react.useCallback(() => {
supervisorRef.current?.complete();
setStatus("completed");
}, []);
const errorCallback = react.useCallback((err) => {
supervisorRef.current?.error(err);
setStatus("failed");
setError(err);
}, []);
const reset = react.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__default.default()) : mitt__default.default();
function useCBController$1({
agentId,
tagConfigs,
callbacks
}) {
const [prompt, setPrompt] = react.useState("");
const [errorMessage, setErrorMessage] = react.useState(null);
const [currentTaskId, setCurrentTaskId] = react.useState(null);
const currentTaskIdRef = react.useRef(null);
const dispatch = useCBDispatch$1();
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] = react.useState([]);
react.useEffect(() => {
if (currentTaskId) {
setMessageBlocks(getTask2(currentTaskId)?.messageBlocks || []);
} else {
setMessageBlocks([]);
}
}, [currentTaskId, tasks]);
const supervisor = useCBTaskSupervisor$1({
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 = react.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 = react.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 = react.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__default.default()) : mitt__default.default();
var CranberrryRenderer$1 = ({ taskId }) => {
const messageBlocks = useAgentSelector$1(
(state) => state.tasks.find((t) => t.id === taskId)?.messageBlocks || []
);
const [liveBlocks, setLiveBlocks] = react.useState(messageBlocks);
react.useEffect(() => {
setLiveBlocks(messageBlocks);
}, [messageBlocks]);
react.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__ */ jsxRuntime.jsx("div", { className: "cranberrry-renderer-messages space-y-2", children: liveBlocks.map((block) => {
const BlockComponent = block.component;
return /* @__PURE__ */ jsxRuntime.jsx(BlockComponent, { ai: block.content }, block.id);
}) });
};
exports.CBAgentReducer = CBAgentReducer;
exports.CranberrryProvider = CranberrryProvider$1;
exports.CranberrryRenderer = CranberrryRenderer$1;
exports.addAgent = addAgent;
exports.addMessageBlock = addMessageBlock;
exports.addTask = addTask;
exports.createAgentSupervisor = createAgentSupervisor;
exports.createCBStore = createCBStore;
exports.getTask = getTask;
exports.getTasksForAgent = getTasksForAgent;
exports.removeTask = removeTask;
exports.setTaskStatus = setTaskStatus;
exports.updateAgent = updateAgent;
exports.updateTask = updateTask;
exports.useAgentSelector = useAgentSelector$1;
exports.useCBAgent = useCBAgent$1;
exports.useCBController = useCBController$1;
exports.useCBDispatch = useCBDispatch$1;
exports.useCBSelector = useCBSelector$1;
exports.useCBTaskManager = useCBTaskManager;
exports.useCBTaskSupervisor = useCBTaskSupervisor$1;
exports.useCranberrryStore = useCranberrryStore$1;
declare function useCBDispatch(): (action: undefined) => void;
declare function useCBSelector<T>(selector: (state: CBState) => T): T;
declare const useAgentSelector: typeof useCBSelector;
declare function useCBAgent(): {
agents: CBAgent[];
getAgentById: (id: string) => CBAgent | undefined;
};
declare function useCBController({ agentId, tagConfigs, callbacks, }: {
agentId: string;
tagConfigs: CBTagConfig[];
callbacks: CBBlockSupervisorCallbacks;
}): {
prompt: string;
setPrompt: react$1.Dispatch<react$1.SetStateAction<string>>;
isRunning: boolean;
error: string | null;
startAgentTask: (newPrompt: string, newMeta?: Record<string, any>) => string;
startExistingAgentTask: (taskId: string) => string;
parseChunk: (chunk: string) => void;
complete: () => void;
reset: () => void;
status: undefined;
tasks: undefined[];
updateTask: (id: string, updates: Partial<undefined>) => void;
removeTask: (id: string) => void;
getTask: (id: string) => undefined | undefined;
setTaskStatus: (id: string, status: undefined) => void;
completeWithError: (err: string) => void;
messageBlocks: CBMessageBlock[];
emitter: Emitter<any>;
};
declare function useCBTaskSupervisor({ callbacks, tagConfigs, emitter, }: {
callbacks: CBBlockSupervisorCallbacks;
tagConfigs: CBTagConfig[];
emitter?: Emitter<any>;
}): {
status: CBTaskStatus;
startTask: (taskId: string) => void;
parseChunk: (chunk: string) => void;
complete: () => void;
error: string | null;
errorCallback: (err: string) => void;
reset: () => void;
};
declare function CranberrryProvider({ store, children, }: {
store: CBStore;
children: ReactNode;
}): react_jsx_runtime.JSX.Element;
declare function useCranberrryStore(): CBStore;
interface CranberrryRendererProps {
taskId: string;
}
declare const CranberrryRenderer: react__default.FC<CranberrryRendererProps>;
export { type CBAction, type CBAgent, type CBAgentID, CBAgentReducer$1 as CBAgentReducer, type CBBlockCallback, type CBBlockProcessor, type CBBlockSupervisorCallbacks, type CBListener, type CBMessageBlock, type CBStartTaskParams, type CBState, type CBStore, type CBTagConfig, type CBTask, type CBTaskCallbacks, type CBTaskStatus, CranberrryProvider, CranberrryRenderer, addAgent$1 as addAgent, addMessageBlock$1 as addMessageBlock, addTask$1 as addTask, createAgentSupervisor$1 as createAgentSupervisor, createCBStore$1 as createCBStore, getTask$1 as getTask, getTasksForAgent$1 as getTasksForAgent, removeTask$1 as removeTask, setTaskStatus$1 as setTaskStatus, updateAgent$1 as updateAgent, updateTask$1 as updateTask, useAgentSelector, useCBAgent, useCBController, useCBDispatch, useCBSelector, useCBTaskManager$1 as useCBTaskManager, useCBTaskSupervisor, useCranberrryStore };