Expand kitchen sink plugin demos
This commit is contained in:
@@ -17,6 +17,8 @@ import {
|
||||
usePluginData,
|
||||
usePluginAction,
|
||||
useHostContext,
|
||||
usePluginStream,
|
||||
usePluginToast,
|
||||
} from "./bridge.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -62,6 +64,8 @@ export function initPluginBridge(
|
||||
usePluginData,
|
||||
usePluginAction,
|
||||
useHostContext,
|
||||
usePluginStream,
|
||||
usePluginToast,
|
||||
|
||||
// Placeholder shared UI components — plugins that use these will get
|
||||
// functional stubs. Full implementations matching the host's design
|
||||
|
||||
@@ -34,6 +34,7 @@ import type {
|
||||
} from "@paperclipai/shared";
|
||||
import { pluginsApi } from "@/api/plugins";
|
||||
import { ApiError } from "@/api/client";
|
||||
import { useToast, type ToastInput } from "@/context/ToastContext";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Bridge error type (mirrors the SDK's PluginBridgeError)
|
||||
@@ -59,6 +60,9 @@ export interface PluginDataResult<T = unknown> {
|
||||
refresh(): void;
|
||||
}
|
||||
|
||||
export type PluginToastInput = ToastInput;
|
||||
export type PluginToastFn = (input: PluginToastInput) => string | null;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Host context type (mirrors the SDK's PluginHostContext)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -359,3 +363,113 @@ export function useHostContext(): PluginHostContext {
|
||||
const { hostContext } = usePluginBridgeContext();
|
||||
return hostContext;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// usePluginToast — concrete implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function usePluginToast(): PluginToastFn {
|
||||
const { pushToast } = useToast();
|
||||
return useCallback(
|
||||
(input: PluginToastInput) => pushToast(input),
|
||||
[pushToast],
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// usePluginStream — concrete implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface PluginStreamResult<T = unknown> {
|
||||
events: T[];
|
||||
lastEvent: T | null;
|
||||
connecting: boolean;
|
||||
connected: boolean;
|
||||
error: Error | null;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export function usePluginStream<T = unknown>(
|
||||
channel: string,
|
||||
options?: { companyId?: string },
|
||||
): PluginStreamResult<T> {
|
||||
const { pluginId, hostContext } = usePluginBridgeContext();
|
||||
const effectiveCompanyId = options?.companyId ?? hostContext.companyId ?? undefined;
|
||||
const [events, setEvents] = useState<T[]>([]);
|
||||
const [lastEvent, setLastEvent] = useState<T | null>(null);
|
||||
const [connecting, setConnecting] = useState<boolean>(Boolean(effectiveCompanyId));
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const sourceRef = useRef<EventSource | null>(null);
|
||||
|
||||
const close = useCallback(() => {
|
||||
sourceRef.current?.close();
|
||||
sourceRef.current = null;
|
||||
setConnecting(false);
|
||||
setConnected(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setEvents([]);
|
||||
setLastEvent(null);
|
||||
setError(null);
|
||||
|
||||
if (!effectiveCompanyId) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({ companyId: effectiveCompanyId });
|
||||
const source = new EventSource(
|
||||
`/api/plugins/${encodeURIComponent(pluginId)}/bridge/stream/${encodeURIComponent(channel)}?${params.toString()}`,
|
||||
{ withCredentials: true },
|
||||
);
|
||||
sourceRef.current = source;
|
||||
setConnecting(true);
|
||||
setConnected(false);
|
||||
|
||||
source.onopen = () => {
|
||||
setConnecting(false);
|
||||
setConnected(true);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
source.onmessage = (event) => {
|
||||
try {
|
||||
const parsed = JSON.parse(event.data) as T;
|
||||
setEvents((current) => [...current, parsed]);
|
||||
setLastEvent(parsed);
|
||||
} catch (nextError) {
|
||||
setError(nextError instanceof Error ? nextError : new Error(String(nextError)));
|
||||
}
|
||||
};
|
||||
|
||||
source.addEventListener("close", () => {
|
||||
source.close();
|
||||
if (sourceRef.current === source) {
|
||||
sourceRef.current = null;
|
||||
}
|
||||
setConnecting(false);
|
||||
setConnected(false);
|
||||
});
|
||||
|
||||
source.onerror = () => {
|
||||
setConnecting(false);
|
||||
setConnected(false);
|
||||
setError(new Error(`Failed to connect to plugin stream "${channel}"`));
|
||||
source.close();
|
||||
if (sourceRef.current === source) {
|
||||
sourceRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
source.close();
|
||||
if (sourceRef.current === source) {
|
||||
sourceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [channel, close, effectiveCompanyId, pluginId]);
|
||||
|
||||
return { events, lastEvent, connecting, connected, error, close };
|
||||
}
|
||||
|
||||
@@ -257,11 +257,11 @@ function getShimBlobUrl(specifier: "react" | "react-dom" | "react-dom/client" |
|
||||
case "sdk-ui":
|
||||
source = `
|
||||
const SDK = globalThis.__paperclipPluginBridge__?.sdkUi ?? {};
|
||||
const { usePluginData, usePluginAction, useHostContext, usePluginStream,
|
||||
const { usePluginData, usePluginAction, useHostContext, usePluginStream, usePluginToast,
|
||||
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
||||
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
||||
Spinner, ErrorBoundary } = SDK;
|
||||
export { usePluginData, usePluginAction, useHostContext, usePluginStream,
|
||||
export { usePluginData, usePluginAction, useHostContext, usePluginStream, usePluginToast,
|
||||
MetricCard, StatusBadge, DataTable, TimeseriesChart,
|
||||
MarkdownBlock, KeyValueList, ActionBar, LogView, JsonTree,
|
||||
Spinner, ErrorBoundary };
|
||||
|
||||
Reference in New Issue
Block a user