@sanity/assist
Version:
You create the instructions; Sanity AI Assist does the rest.
1,149 lines • 171 kB
JavaScript
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
import { pathToString, getVersionFromId, getPublishedId, isVersionId, useEditState, useCurrentUser, useClient, typed, isObjectSchemaType, stringToPath, isKeySegment, isArraySchemaType, useSchema, FormFieldHeaderText, PatchEvent, unset, getVersionId, getDraftId, useColorSchemeValue, useFormCallbacks, useDocumentStore, useDocumentPresence, createPatchChannel, FormBuilder, fromMutationPatches, StatusButton, PresenceOverlay, VirtualizerScrollInstanceProvider, isDocumentSchemaType, useSyncState, set, useWorkspaceSchemaId, FormCallbacksProvider, FormInput, setIfMissing, insert, ObjectInputMember, isArrayOfObjectsSchemaType, defineType, defineField, defineArrayMember, definePlugin } from "sanity";
import { useToast, useLayer, Dialog, Stack, Flex, Tooltip, Text, TextArea, Button, Badge, Popover, Card, Box, ErrorBoundary, focusFirstDescendant, Spinner, Container, Autocomplete, Breadcrumbs, useClickOutside, useGlobalKeyDown, useTheme, rgba, Radio, Checkbox, ThemeProvider, MenuButton, Menu, MenuItem, Switch, Label } from "@sanity/ui";
import { useRef, useState, useEffect, useMemo, createContext, useContext, useCallback, useId, forwardRef, createElement, useReducer } from "react";
import { useDocumentPane, usePaneRouter, DocumentInspectorHeader, DocumentPaneProvider } from "sanity/structure";
import { minutesToMilliseconds, isAfter, addSeconds, formatDistanceToNow } from "date-fns";
import { DocumentIcon, LinkIcon, ImageIcon, BlockContentIcon, OlistIcon, BlockquoteIcon, StringIcon, PlayIcon, SparklesIcon, ArrowRightIcon, CheckmarkIcon, SearchIcon, SyncIcon, ErrorOutlineIcon, CheckmarkCircleIcon, ClockIcon, CloseCircleIcon, RetryIcon, CloseIcon, icons, TranslateIcon, LockIcon, ControlsIcon, ArrowLeftIcon, TokenIcon, DocumentTextIcon, ThListIcon, CodeIcon, ComposeIcon } from "@sanity/icons";
import { extractWithPath } from "@sanity/mutator";
import { keyframes, styled } from "styled-components";
import { tap, mergeMap, share, take, filter, distinctUntilChanged, catchError } from "rxjs/operators";
import { get } from "lodash-es";
import isEqual from "react-fast-compare";
import { defer, throwError, of, partition, merge, switchMap, delay } from "rxjs";
import { exhaustMapToWithTrailing } from "rxjs-exhaustmap-with-trailing";
function hasOverflowScroll(el) {
const overflow = getComputedStyle(el).overflow;
return overflow.includes("auto") || overflow.includes("hidden") || overflow.includes("scroll");
}
function useRegionRects() {
const ref = useRef(null), [relativeBoundsRect, setRelativeBoundsRect] = useState(null), [relativeElementRect, setRelativeElementRect] = useState(null), [boundsScroll, setBoundsScroll] = useState({ x: 0, y: 0 }), [scroll, setScroll] = useState({ x: 0, y: 0 }), boundsScrollXRef = useRef(0), boundsScrollYRef = useRef(0), elementScrollXRef = useRef(0), elementScrollYRef = useRef(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
const scrollParents = [];
let parent = el.parentElement;
for (; parent && parent !== document.body; )
hasOverflowScroll(parent) && scrollParents.push(parent), parent = parent.parentElement;
function handleResize() {
const boundsRect = scrollParents[0]?.getBoundingClientRect() || {
x: 0,
y: 0,
width: window.innerWidth,
height: window.innerHeight
}, domRect = el.getBoundingClientRect();
setRelativeBoundsRect({
x: boundsRect.x + boundsScrollXRef.current,
y: boundsRect.y + boundsScrollYRef.current,
w: boundsRect.width,
h: boundsRect.height
}), setRelativeElementRect({
x: domRect.x + elementScrollXRef.current,
y: domRect.y + elementScrollYRef.current,
w: domRect.width,
h: domRect.height
});
}
function handleScroll() {
let scrollX = window.scrollX, scrollY = window.scrollY;
for (const scrollParent2 of scrollParents)
scrollX += scrollParent2.scrollLeft, scrollY += scrollParent2.scrollTop;
const scrollParent = scrollParents[0];
boundsScrollXRef.current = scrollX - (scrollParent?.scrollLeft || window.scrollX), boundsScrollYRef.current = scrollY - (scrollParent?.scrollTop || window.scrollY), setBoundsScroll({
x: boundsScrollXRef.current,
y: boundsScrollYRef.current
}), elementScrollXRef.current = scrollX, elementScrollYRef.current = scrollY, setScroll({ x: scrollX, y: scrollY });
}
window.addEventListener("scroll", handleScroll, { passive: !0 });
const ro = new ResizeObserver(handleResize);
ro.observe(el);
for (const scrollParent of scrollParents)
scrollParent.addEventListener("scroll", handleScroll, { passive: !0 }), ro.observe(scrollParent);
return handleScroll(), () => {
ro.unobserve(el);
for (const scrollParent of scrollParents)
ro.unobserve(scrollParent), scrollParent.removeEventListener("scroll", handleScroll);
ro.disconnect(), window.removeEventListener("scroll", handleScroll);
};
}, []);
const bounds = useMemo(
() => relativeBoundsRect && {
x: relativeBoundsRect.x - boundsScroll.x,
y: relativeBoundsRect.y - boundsScroll.y,
w: relativeBoundsRect.w,
h: relativeBoundsRect.h
},
[relativeBoundsRect, boundsScroll]
), element = useMemo(
() => relativeElementRect && {
x: relativeElementRect.x - scroll.x,
y: relativeElementRect.y - scroll.y,
w: relativeElementRect.w,
h: relativeElementRect.h
},
[relativeElementRect, scroll]
);
return { bounds, element, ref };
}
function ConnectorRegion(props) {
const { children, onRectsChange, ...restProps } = props, { bounds, element, ref } = useRegionRects();
return useEffect(() => {
onRectsChange?.(bounds && element ? { bounds, element } : null);
}, [bounds, element, onRectsChange]), /* @__PURE__ */ jsx("div", { ...restProps, ref, children });
}
const ConnectorsStoreContext = createContext(null);
function useConnectorsStore() {
const store = useContext(ConnectorsStoreContext);
if (!store)
throw new Error("Missing connectors store context");
return store;
}
function ConnectFromRegion(props) {
const { children, _key: key, zIndex, ...restProps } = props, store = useConnectorsStore(), [rects, setRects] = useState(null);
return useEffect(() => store.from.subscribe(key, { zIndex }), [key, store, zIndex]), useEffect(() => {
rects && store.from.next(key, rects);
}, [key, rects, store]), /* @__PURE__ */ jsx(ConnectorRegion, { ...restProps, onRectsChange: setRects, children });
}
function createConnectorsStore() {
const configKeys = [], fieldKeys = [], channels = {
from: /* @__PURE__ */ new Map(),
to: /* @__PURE__ */ new Map()
}, payloads = {
from: /* @__PURE__ */ new Map(),
to: /* @__PURE__ */ new Map()
}, observers = [];
function notifyObservers() {
const connectors = [];
for (const key of configKeys) {
const toRects = channels.to.get(key), toPayload = payloads.from.get(key), fromRects = channels.from.get(key), fromPayload = payloads.from.get(key);
toRects && fromRects && connectors.push({
key,
from: { ...fromRects, payload: fromPayload },
to: { ...toRects, payload: toPayload }
});
}
for (const observer of observers)
observer(connectors);
}
return {
to: {
subscribe(key, payload) {
return channels.to.set(key, null), payloads.to.set(key, payload), configKeys.push(key), () => {
channels.to.delete(key), payloads.to.delete(key);
const idx = configKeys.indexOf(key);
idx > -1 && configKeys.splice(idx, 1), notifyObservers();
};
},
next(key, rects) {
channels.to.set(key, rects), fieldKeys.includes(key) && notifyObservers();
}
},
connectors: {
subscribe(observer) {
return observers.push(observer), () => {
const idx = observers.indexOf(observer);
idx > -1 && observers.splice(idx, 1);
};
}
},
from: {
subscribe(key, payload) {
return channels.from.set(key, null), payloads.from.set(key, payload), fieldKeys.push(key), () => {
channels.from.delete(key), payloads.from.delete(key);
const idx = fieldKeys.indexOf(key);
idx > -1 && fieldKeys.splice(idx, 1), notifyObservers();
};
},
next(key, rects) {
channels.from.set(key, rects), configKeys.includes(key) && notifyObservers();
}
}
};
}
function ConnectorsProvider(props) {
const { children, onConnectorsChange } = props, store = useMemo(() => createConnectorsStore(), []);
return useEffect(
() => onConnectorsChange && store.connectors.subscribe(onConnectorsChange),
[onConnectorsChange, store]
), /* @__PURE__ */ jsx(ConnectorsStoreContext.Provider, { value: store, children });
}
function getConnectorLinePoint(options2, rect, bounds) {
const centerY = rect.y + rect.h / 2, isAbove = rect.y + rect.h < bounds.y + options2.arrow.marginY, isBelow = rect.y > bounds.y + bounds.h - options2.arrow.marginY;
return {
bounds,
x: rect.x,
y: centerY,
centerY,
startY: rect.y + options2.path.marginY,
endY: rect.y + rect.h - options2.path.marginY,
isAbove,
isBelow,
outOfBounds: isAbove || isBelow
};
}
function mapConnectorToLine(options2, connector) {
const fromBounds = {
y: connector.from.bounds.y + options2.arrow.threshold,
// bottom: connector.from.bounds.y + connector.from.bounds.h - options.arrow.threshold,
x: connector.from.bounds.x,
// right: connector.from.bounds.x + connector.from.bounds.w,
w: connector.from.bounds.w,
h: connector.from.bounds.h - options2.arrow.threshold * 2
}, from = getConnectorLinePoint(options2, connector.from.element, fromBounds);
from.x = connector.from.element.x + connector.from.element.w;
const fromBottom = fromBounds.y + fromBounds.h, toBounds = {
y: connector.to.bounds.y + options2.arrow.threshold,
// bottom: connector.to.bounds.y + connector.to.bounds.h - options.arrow.threshold,
x: connector.to.bounds.x,
// right: connector.to.bounds.x + connector.to.bounds.w,
w: connector.to.bounds.w,
h: connector.to.bounds.h - options2.arrow.threshold * 2
}, toBottom = toBounds.y + toBounds.h, to = getConnectorLinePoint(options2, connector.to.element, toBounds), maxStartY = Math.max(to.startY, from.startY);
return from.y = Math.min(maxStartY, from.endY), from.y < toBounds.y ? from.y = Math.min(toBounds.y, from.endY) : from.y > toBottom && (from.y = Math.max(toBottom, from.startY)), to.y = Math.min(maxStartY, to.endY), to.y < fromBounds.y ? to.y = Math.min(fromBounds.y, to.endY) : to.y > fromBottom && (to.y = Math.max(fromBottom, to.startY)), from.y = Math.min(Math.max(from.y, fromBounds.y), fromBottom), to.y = Math.min(Math.max(to.y, toBounds.y), toBottom), { from, to };
}
const assistFormId = "assist", assistDocumentIdPrefix = "sanity.assist.schemaType.", assistDocumentStatusIdPrefix = "sanity.assist.status.", assistSchemaIdPrefix = "sanity.assist.schema.", assistDocumentTypeName = "sanity.assist.schemaType.annotations", assistFieldTypeName = "sanity.assist.schemaType.field", instructionTypeName = "sanity.assist.instruction", promptTypeName = "sanity.assist.instruction.prompt", userInputTypeName = "sanity.assist.instruction.userInput", instructionContextTypeName = "sanity.assist.instruction.context", fieldReferenceTypeName = "sanity.assist.instruction.fieldRef", contextDocumentTypeName = "assist.instruction.context", assistTasksStatusTypeName = "sanity.assist.task.status", instructionTaskTypeName = "sanity.assist.instructionTask", fieldPresenceTypeName = "sanity.assist.instructionTask.presence", assistSerializedTypeName = "sanity.assist.serialized.type", assistSerializedFieldTypeName = "sanity.assist.serialized.field", outputFieldTypeName = "sanity.assist.output.field", outputTypeTypeName = "sanity.assist.output.type", fieldPathParam = "pathKey", instructionParam = "instruction", documentRootKey = "<document>";
function usePathKey(path) {
return useMemo(() => getPathKey(path), [path]);
}
function getPathKey(path) {
return path.length ? Array.isArray(path) ? pathToString(path) : path : documentRootKey;
}
function getInstructionTitle(instruction2) {
return instruction2?.title ?? "Untitled";
}
function isDefined(t) {
return t != null;
}
function isPortableTextArray(type) {
return type.of.find((t) => isType(t, "block"));
}
function isType(schemaType, typeName) {
return schemaType.name === typeName ? !0 : schemaType.type ? isType(schemaType.type, typeName) : !1;
}
function isImage(schemaType) {
return isType(schemaType, "image");
}
function getDescriptionFieldOption(schemaType) {
if (!schemaType)
return;
const descriptionField = schemaType.options?.aiAssist?.imageDescriptionField;
return typeof descriptionField == "string" ? {
path: descriptionField,
updateOnImageChange: !0
} : descriptionField ? {
path: descriptionField.path,
updateOnImageChange: descriptionField.updateOnImageChange ?? !0
} : getDescriptionFieldOption(schemaType.type);
}
function getImageInstructionFieldOption(schemaType) {
return schemaType ? schemaType.options?.aiAssist?.imageInstructionField || getImageInstructionFieldOption(schemaType.type) : void 0;
}
function isSchemaAssistEnabled(type) {
return !type.options?.aiAssist?.exclude;
}
function isAssistSupported(type) {
return !isSchemaAssistEnabled(type) || isDisabled(type) ? !1 : type.jsonType === "array" ? !type.of.every((t) => isDisabled(t)) : type.jsonType === "object" ? !type.fields.every((field) => isDisabled(field.type)) || /* to allow attaching custom actions on fieldless images */
isType(type, "image") : !0;
}
function isDisabled(type) {
return !isSchemaAssistEnabled(type) || isUnsupportedType(type);
}
function isUnsupportedType(type) {
return type.name === "sanity.imageCrop" || type.name === "sanity.imageHotspot" || isType(type, "globalDocumentReference") || isType(type, "reference") && !type?.options?.aiAssist?.embeddingsIndex || isType(type, "crossDatasetReference") || isType(type, "file");
}
const FirstAssistedPathContext = createContext(void 0);
function FirstAssistedPathProvider(props) {
const { members } = props, firstAssistedPath = useMemo(() => {
const firstAssisted = members.find(
(member) => member.kind === "field" && isAssistSupported(member.field.schemaType)
);
return firstAssisted?.field.path ? pathToString(firstAssisted?.field.path) : void 0;
}, [members]);
return /* @__PURE__ */ jsx(FirstAssistedPathContext.Provider, { value: firstAssistedPath, children: props.children });
}
const releaseAnnouncementUrl = "https://www.sanity.io/blog/sanity-ai-assist-announcement?utm_source=sanity-assist-plugin&utm_medium=organic_social&utm_campaign=ai-assist&utm_content=", instructionGuideUrl = "https://sanity.io/guides/getting-started-with-ai-assist-instructions?utm_source=sanity-assist-plugin&utm_medium=organic_social&utm_campaign=ai-assist&utm_content=", giveFeedbackUrl = "https://forms.gle/Kwz7CThxGeA2GiEU8", salesUrl = "https://www.sanity.io/contact/sales?utm_source=sanity-assist-plugin&utm_medium=organic_social&utm_campaign=ai-assist&utm_content=", packageName = "@sanity/assist", pluginTitle = "Sanity AI Assist", pluginTitleShort = "AI Assist", maxHistoryVisibilityMs = minutesToMilliseconds(30), illegalIdChars = /[^a-zA-Z0-9._-]/g;
function assistDocumentId(documentType) {
return `${assistDocumentIdPrefix}${documentType}`.replace(illegalIdChars, "_");
}
function assistTasksStatusId(documentId) {
return isVersionId(documentId) ? `${assistDocumentStatusIdPrefix}${getVersionFromId(documentId)}.${getPublishedId(documentId)}` : `${assistDocumentStatusIdPrefix}${getPublishedId(documentId)}`;
}
function useDocumentState(id, docType) {
const state = useEditState(id, docType);
return state.draft || state.published;
}
function useStudioAssistDocument({
documentId,
schemaType,
initDoc
}) {
const documentTypeName = schemaType.name, currentUser = useCurrentUser(), assistDocument = useDocumentState(
assistDocumentId(documentTypeName),
assistDocumentTypeName
), assistTasksStatus = useDocumentState(
assistTasksStatusId(documentId ?? ""),
assistTasksStatusTypeName
), client = useClient({ apiVersion: "2023-01-01" });
return useEffect(() => {
!assistDocument && initDoc && client.createIfNotExists({
_id: assistDocumentId(documentTypeName),
_type: assistDocumentTypeName
}).catch(() => {
});
}, [client, assistDocument, documentTypeName, initDoc]), useMemo(() => {
if (!assistDocument)
return;
const tasks = assistTasksStatus?.tasks ?? [], fields = (assistDocument?.fields ?? []).map((assistField) => ({
...assistField,
tasks: tasks.filter((task) => task.path === assistField.path),
instructions: assistField.instructions?.filter((p) => !p.userId || p.userId === currentUser?.id).map((instruction2) => asStudioInstruction(instruction2, tasks))
}));
return typed({
...assistDocument,
tasks: tasks?.map((task) => {
const instruction2 = fields.find((f) => f.path === task.path)?.instructions?.find((i) => i._key === task.instructionKey);
return {
...task,
instruction: instruction2
};
}),
fields
});
}, [assistDocument, assistTasksStatus, currentUser]);
}
function asStudioInstruction(instruction2, run) {
return {
...instruction2,
tasks: run.filter((task) => task.instructionKey === instruction2._key).filter(
(task) => task.started && (/* @__PURE__ */ new Date()).getTime() - new Date(task.started).getTime() < maxHistoryVisibilityMs
)
};
}
const NO_TASKS = [];
function useInstructionToaster(documentId, documentSchemaType) {
const assistDocument = useStudioAssistDocument({ documentId, schemaType: documentSchemaType }), assistDocLoaded = !!assistDocument, currentUser = useCurrentUser(), toast = useToast(), tasks = assistDocument?.tasks, previousTasks = useRef("initial");
useEffect(() => {
if (assistDocLoaded) {
if (previousTasks.current !== "initial") {
const prevTaskByKey = Object.fromEntries(
(previousTasks.current ?? NO_TASKS).map((run) => [run._key, run])
);
tasks?.filter((task) => task.startedByUserId === currentUser?.id).filter((task) => {
const prevTask = prevTaskByKey[task._key];
return !prevTask && task.ended || !prevTask?.ended && task.ended;
}).filter((task) => task.ended && isAfter(addSeconds(new Date(task.ended), 30), /* @__PURE__ */ new Date()))?.forEach((task) => {
const title = task.title ?? getInstructionTitle(task.instruction);
task.reason === "error" ? toast.push({
title: `Failed: ${title}`,
status: "error",
description: `Instruction failed. ${task.message ?? ""}`,
closable: !0,
duration: 1e4
}) : task.reason === "timeout" ? toast.push({
title: `Timeout: ${title}`,
status: "error",
description: "Instruction timed out.",
closable: !0
}) : task.reason === "success" ? toast.push({
title: `Success: ${title}`,
status: "success",
description: "Instruction completed.",
closable: !0
}) : task.reason === "aborted" && toast.push({
title: `Canceled: ${title}`,
status: "warning",
description: "Instruction canceled.",
closable: !0
});
});
}
previousTasks.current = tasks;
}
}, [tasks, previousTasks, toast, currentUser, assistDocLoaded]);
}
function AssistDocumentInputWrapper(props) {
if (!isType(props.schemaType, "document") && props.id !== "root" && props.id !== assistFormId)
return /* @__PURE__ */ jsx(AssistInput, { ...props });
const documentId = props.value?._id;
return documentId ? /* @__PURE__ */ jsx(AssistDocumentInput, { ...props, documentId }) : props.renderDefault(props);
}
function AssistDocumentInput({ documentId, ...props }) {
useInstructionToaster(documentId, props.schemaType);
const schemaType = useMemo(() => props.schemaType.name !== assistDocumentTypeName ? props.schemaType : {
...props.schemaType,
type: {
...props.schemaType.type,
// compatability with i18nArrays plugin that requires this to be document
name: "document"
}
}, [props.schemaType]);
return /* @__PURE__ */ jsx(FirstAssistedPathProvider, { members: props.members, children: props.renderDefault({ ...props, schemaType }) });
}
function AssistInput(props) {
const { zIndex } = useLayer(), { paneKey } = useDocumentPane(), pathKey = usePathKey(props.path);
return /* @__PURE__ */ jsx(ConnectFromRegion, { _key: `${paneKey}_${pathKey}`, zIndex, style: { minWidth: 0 }, children: props.renderDefault(props) });
}
const AssistDocumentContext = createContext(
void 0
);
function useAssistDocumentContext() {
const context = useContext(AssistDocumentContext);
if (!context)
throw new Error("AssistDocumentContext value is missing");
return context;
}
const SelectedFieldContext = createContext(void 0), SelectedFieldContextProvider = SelectedFieldContext.Provider, maxDepth = 6;
function getTypeIcon(schemaType) {
let t = schemaType;
for (; t; ) {
if (t.icon) return t.icon;
t = t.type;
}
return isType(schemaType, "slug") ? LinkIcon : isType(schemaType, "image") ? ImageIcon : schemaType.jsonType === "array" && isPortableTextArray(schemaType) ? BlockContentIcon : schemaType.jsonType === "array" ? OlistIcon : schemaType.jsonType === "object" ? BlockquoteIcon : schemaType.jsonType === "string" ? StringIcon : DocumentIcon;
}
function asFieldRefsByTypePath(fieldRefs) {
return fieldRefs.reduce(
(acc, ref) => ({ ...acc, [ref.key]: ref }),
{}
);
}
function getFieldRefsWithDocument(schemaType) {
const fields = getFieldRefs(schemaType);
return [
{
key: documentRootKey,
icon: schemaType.icon ?? DocumentIcon,
title: "The entire document",
path: [],
schemaType
},
...fields
];
}
function getFieldRefs(schemaType, parent, depth = 0) {
return depth >= maxDepth ? [] : schemaType.fields.filter((f) => !f.name.startsWith("_")).flatMap((field) => {
const path = parent ? [...parent.path, field.name] : [field.name], title = field.type.title ?? field.name, fieldRef = {
key: patchableKey(pathToString(path)),
path,
title: parent ? [parent.title, title].join(" / ") : title,
schemaType: field.type,
icon: getTypeIcon(field.type)
}, fields = field.type.jsonType === "object" ? getFieldRefs(field.type, fieldRef, depth + 1) : [], syntheticFields = field.type.jsonType === "array" ? getSyntheticFields(field.type, fieldRef, depth + 1) : [];
return isAssistSupported(field.type) ? [fieldRef, ...fields, ...syntheticFields] : [...fields, ...syntheticFields];
});
}
function getSyntheticFields(schemaType, parent, depth = 0) {
return depth >= maxDepth ? [] : schemaType.of.filter((itemType) => !isType(itemType, "block")).flatMap((itemType) => {
const segment = { _key: itemType.name }, title = itemType.title ?? itemType.name, path = parent ? [...parent.path, segment] : [segment], fieldRef = {
key: patchableKey(pathToString(path)),
path,
title: parent ? [parent.title, title].join(" / ") : title,
schemaType: itemType,
icon: getTypeIcon(itemType),
synthetic: !0
}, fields = itemType.jsonType === "object" ? getFieldRefs(itemType, fieldRef, depth + 1) : [];
return isAssistSupported(itemType) ? [fieldRef, ...fields] : fields;
});
}
function getTypePath(doc, pathString) {
if (!pathString)
return;
const path = stringToPath(pathString), currentPath = [];
let valid = !0;
const syntheticPath = path.map((segment) => {
if (currentPath.push(segment), isKeySegment(segment)) {
const match = extractWithPath(pathToString(currentPath), doc)[0], value = match?.value;
if (match && value && typeof value == "object" && "_type" in value)
return { _key: value._type };
valid = !1;
}
return segment;
});
return valid ? patchableKey(pathToString(syntheticPath)) : void 0;
}
function patchableKey(pathKey) {
return pathKey.replace(/[=]=/g, ":").replace(/[[\]]/g, "|").replace(/"/g, "");
}
function useTypePath(doc, pathString) {
return useMemo(() => getTypePath(doc, pathString), [doc, pathString]);
}
function useSelectedField(documentSchemaType, path) {
const selectableFields = useMemo(
() => documentSchemaType && isObjectSchemaType(documentSchemaType) ? getFieldRefsWithDocument(documentSchemaType) : [],
[documentSchemaType]
);
return useMemo(() => path ? selectableFields?.find((f) => f.key === path) : void 0, [selectableFields, path]);
}
function getFieldTitle(field) {
const schemaType = field?.schemaType;
return field?.title ?? schemaType?.title ?? schemaType?.name ?? "Untitled";
}
function useAiPaneRouter() {
const paneRouter = usePaneRouter();
return useMemo(
() => ({ ...paneRouter, params: paneRouter.params ?? {} }),
[paneRouter]
);
}
const AiAssistanceConfigContext = createContext({});
function useAiAssistanceConfig() {
const context = useContext(AiAssistanceConfigContext);
if (!context)
throw new Error("Missing AiAssistanceConfigContext");
return context;
}
function AiAssistanceConfigProvider(props) {
const [status, setStatus] = useState(), [error, setError] = useState(), apiClient = useApiClient(props.config?.__customApiClient), { getInstructStatus, loading: statusLoading } = useGetInstructStatus(apiClient), { initInstruct, loading: initLoading } = useInitInstruct(apiClient);
useEffect(() => {
getInstructStatus().then((s) => setStatus(s)).catch((e) => {
console.error(e), setError(e);
});
}, [getInstructStatus]);
const init = useCallback(async () => {
setError(void 0);
try {
await initInstruct();
const status2 = await getInstructStatus();
setStatus(status2);
} catch (e) {
console.error("Failed to init ai assistance", e), setError(e);
}
}, [initInstruct, getInstructStatus, setStatus]), { config, children } = props, context = useMemo(() => ({
config,
status,
statusLoading,
init,
initLoading,
error
}), [config, status, init, statusLoading, initLoading, error]);
return /* @__PURE__ */ jsx(AiAssistanceConfigContext.Provider, { value: context, children });
}
const hiddenTypes = [
"any",
"array",
"block",
"boolean",
"crossDatasetReference",
"date",
"datetime",
"document",
"email",
"file",
"globalDocumentReference",
"image",
"number",
"object",
"reference",
"span",
"string",
"text",
"url",
"slug",
"geopoint",
"sanity.assetSourceData",
"sanity.imageAsset",
"sanity.fileAsset",
"sanity.imageCrop",
"sanity.imageHotspot",
"sanity.imageMetadata",
"sanity.imageDimensions",
"sanity.imagePalette",
"sanity.imagePaletteSwatch",
assistSerializedTypeName,
assistSerializedFieldTypeName,
"sanity-agent.job.document"
], inlineTypes = ["document", "object", "image", "file"];
function serializeSchema(schema, options2) {
const list = schema.getTypeNames().filter((t) => !(hiddenTypes.includes(t) || t.startsWith("sanity."))).map((t) => schema.get(t)).filter((t) => !!t).map((t) => getSchemaStub(t, schema, options2)).filter((t) => !("to" in t && t.to && !t.to.length || "of" in t && t.of && !t.of.length || "fields" in t && t.fields && !t.fields.length));
return list.sort((a, b) => (a?.name ?? "").localeCompare(b?.name ?? "")), list;
}
function getSchemaStub(schemaType, schema, options2) {
if (!schemaType.type?.name)
throw console.error("Missing type name", schemaType.type), new Error("Type is missing name!");
const baseSchema = {
// we dont need type or id when we send using POST, so leave these out to save bandwidth
...options2?.leanFormat ? {} : { _id: `${assistSchemaIdPrefix}${schemaType.name}`, _type: assistSerializedTypeName },
name: schemaType.name,
title: schemaType.title,
type: schemaType.type.name,
...getBaseFields(schema, schemaType, schemaType.type.name, options2)
};
return removeUndef(baseSchema);
}
function getBaseFields(schema, type, typeName, options2) {
const schemaOptions = removeUndef({
imagePromptField: type.options?.aiAssist?.imageInstructionField,
embeddingsIndex: type.options?.aiAssist?.embeddingsIndex
});
return removeUndef({
options: Object.keys(schemaOptions).length ? schemaOptions : void 0,
values: Array.isArray(type?.options?.list) ? type?.options?.list.map(
(v) => typeof v == "string" ? v : v.value ?? `${v.title}`
) : void 0,
of: "of" in type && typeName === "array" ? arrayOf(type, schema, options2) : void 0,
to: "to" in type && typeName === "reference" ? refToTypeNames(type) : void 0,
fields: "fields" in type && inlineTypes.includes(typeName) ? serializeFields(schema, type, options2) : void 0,
annotations: typeName === "block" && "fields" in type ? serializeAnnotations(type, schema, options2) : void 0,
inlineOf: typeName === "block" && "fields" in type ? serializeInlineOf(type, schema, options2) : void 0,
hidden: typeof type.hidden == "function" ? "function" : type.hidden ? !0 : void 0,
readOnly: typeof type.readOnly == "function" ? "function" : type.readOnly ? !0 : void 0
});
}
function serializeFields(schema, schemaType, options2) {
return (schemaType.fieldsets ? schemaType.fieldsets.flatMap(
(fs) => fs.single ? fs.field : fs.fields.map((f) => ({
...f,
type: {
...f.type,
// if fieldset is (conditionally) hidden, the field must be considered the same way
// ie, if the field does not show up in conditionalMembers, it is hidden
// regardless of weather or not it is the field function or the fieldset function that hides it
hidden: typeof fs.hidden == "function" ? fs.hidden : fs.hidden ? !0 : f.type.hidden
}
}))
) : schemaType.fields).filter((f) => !["sanity.imageHotspot", "sanity.imageCrop"].includes(f.type?.name ?? "")).filter((f) => isAssistSupported(f.type)).map((field) => serializeMember(schema, field.type, field.name, options2));
}
function serializeMember(schema, type, name, options2) {
const typeName = schema.get(type?.name) ? type.name : type.type?.name ?? "";
return removeUndef({
...options2?.leanFormat ? {} : { _type: assistSerializedFieldTypeName },
name,
type: typeName,
title: type.title,
...getBaseFields(schema, type, typeName, options2)
});
}
function serializeInlineOf(blockSchemaType, schema, options2) {
const childrenType = blockSchemaType.fields.find((f) => f.name === "children")?.type;
if (!(!childrenType || !isArraySchemaType(childrenType)))
return arrayOf(
{
of: childrenType.of.filter((t) => !isType(t, "span"))
},
schema,
options2
);
}
function serializeAnnotations(blockSchemaType, schema, options2) {
const marksType = blockSchemaType.fields.find((f) => f.name === "markDefs")?.type;
if (!(!marksType || !isArraySchemaType(marksType)))
return arrayOf(marksType, schema, options2);
}
function arrayOf(arrayType, schema, options2) {
return arrayType.of.filter((type) => isAssistSupported(type)).map((t) => serializeMember(schema, t, t.name, options2));
}
function refToTypeNames(type) {
return type.to.map((t) => ({
type: typed(t.name)
}));
}
function removeUndef(obj) {
return Object.keys(obj).forEach((key) => obj[key] === void 0 ? delete obj[key] : {}), obj;
}
const basePath = "/assist/tasks/instruction", API_VERSION_WITH_EXTENDED_TYPES = "2025-04-01";
function canUseAssist(status) {
return status?.enabled && status.initialized && status.validToken;
}
function useApiClient(customApiClient) {
const client = useClient({ apiVersion: API_VERSION_WITH_EXTENDED_TYPES });
return useMemo(
() => customApiClient ? customApiClient(client) : client,
[client, customApiClient]
);
}
function useTranslate(apiClient) {
const [loading, setLoading] = useState(!1), user = useCurrentUser(), schema = useSchema(), types = useMemo(() => serializeSchema(schema, { leanFormat: !0 }), [schema]), toast = useToast(), translate = useCallback(
({
documentId,
languagePath,
styleguide,
translatePath,
fieldLanguageMap,
conditionalMembers
}) => {
setLoading(!0);
async function run() {
return apiClient.request({
method: "POST",
url: `/assist/tasks/translate/${apiClient.config().dataset}?projectId=${apiClient.config().projectId}`,
body: {
documentId,
types,
languagePath,
userStyleguide: await styleguide(),
fieldLanguageMap,
conditionalMembers,
translatePath: translatePath.length === 0 ? documentRootKey : pathToString(translatePath),
userId: user?.id
}
});
}
return run().catch((e) => {
throw toast.push({
status: "error",
title: "Translate failed",
description: e.message
}), setLoading(!1), e;
}).finally(() => {
setTimeout(() => {
setLoading(!1);
}, 2e3);
});
},
[setLoading, apiClient, toast, user, types]
);
return useMemo(
() => ({
translate,
loading
}),
[translate, loading]
);
}
function useGenerateCaption(apiClient) {
const [loading, setLoading] = useState(!1), user = useCurrentUser(), schema = useSchema(), types = useMemo(() => serializeSchema(schema, { leanFormat: !0 }), [schema]), toast = useToast(), generateCaption = useCallback(
({ path, documentId }) => (setLoading(!0), apiClient.request({
method: "POST",
url: `/assist/tasks/generate-caption/${apiClient.config().dataset}?projectId=${apiClient.config().projectId}`,
body: {
path,
documentId,
types,
userId: user?.id
}
}).catch((e) => {
throw toast.push({
status: "error",
title: "Generate image description failed",
description: e.message
}), setLoading(!1), e;
}).finally(() => {
setTimeout(() => {
setLoading(!1);
}, 2e3);
})),
[setLoading, apiClient, toast, user, types]
);
return useMemo(
() => ({
generateCaption,
loading
}),
[generateCaption, loading]
);
}
function useGenerateImage(apiClient) {
const [loading, setLoading] = useState(!1), user = useCurrentUser(), schema = useSchema(), types = useMemo(() => serializeSchema(schema, { leanFormat: !0 }), [schema]), toast = useToast(), generateImage = useCallback(
({ path, documentId }) => (setLoading(!0), apiClient.request({
method: "POST",
url: `/assist/tasks/generate-image/${apiClient.config().dataset}?projectId=${apiClient.config().projectId}`,
body: {
path,
documentId,
types,
userId: user?.id
}
}).catch((e) => {
throw toast.push({
status: "error",
title: "Generate image from prompt failed",
description: e.message
}), setLoading(!1), e;
}).finally(() => {
setTimeout(() => {
setLoading(!1);
}, 2e3);
})),
[setLoading, apiClient, toast, user, types]
);
return useMemo(
() => ({
generateImage,
loading
}),
[generateImage, loading]
);
}
function useGetInstructStatus(apiClient) {
const [loading, setLoading] = useState(!0), getInstructStatus = useCallback(async () => {
setLoading(!0);
const projectId = apiClient.config().projectId;
try {
return await apiClient.request({
method: "GET",
url: `${basePath}/${apiClient.config().dataset}/status?projectId=${projectId}`
});
} finally {
setLoading(!1);
}
}, [setLoading, apiClient]);
return {
loading,
getInstructStatus
};
}
function useInitInstruct(apiClient) {
const [loading, setLoading] = useState(!1), initInstruct = useCallback(() => (setLoading(!0), apiClient.request({
method: "GET",
url: `${basePath}/${apiClient.config().dataset}/init?projectId=${apiClient.config().projectId}`
}).finally(() => {
setLoading(!1);
})), [setLoading, apiClient]);
return {
loading,
initInstruct
};
}
function useRunInstructionApi(apiClient) {
const toast = useToast(), [loading, setLoading] = useState(!1), user = useCurrentUser(), schema = useSchema(), types = useMemo(() => serializeSchema(schema, { leanFormat: !0 }), [schema]), {
config: { assist: assistConfig }
} = useAiAssistanceConfig(), runInstruction = useCallback(
(request) => {
if (!user) {
toast.push({
status: "error",
title: "Unable to get user for instruction."
});
return;
}
setLoading(!0);
const { timeZone, locale } = Intl.DateTimeFormat().resolvedOptions(), defaultLocaleSettings = { timeZone, locale }, localeSettings = assistConfig?.localeSettings?.({ user, defaultSettings: defaultLocaleSettings }) ?? defaultLocaleSettings;
return apiClient.request({
method: "POST",
url: `${basePath}/${apiClient.config().dataset}?projectId=${apiClient.config().projectId}`,
body: {
...request,
types,
userId: user?.id,
localeSettings,
maxPathDepth: assistConfig?.maxPathDepth
}
}).catch((e) => {
throw toast.push({
status: "error",
title: "Instruction failed",
description: e.message
}), e;
}).finally(() => {
setLoading(!1);
});
},
[apiClient, types, user, toast, assistConfig]
);
return useMemo(
() => ({
runInstruction,
loading
}),
[runInstruction, loading]
);
}
const NO_INPUT = {}, RunInstructionContext = createContext({
runInstruction: () => {
},
getUserInput: async () => {
},
instructionLoading: !1
});
function useRunInstruction() {
return useContext(RunInstructionContext);
}
function isUserInputBlock(block) {
return block._type === userInputTypeName;
}
function RunInstructionProvider(props) {
const { config } = useAiAssistanceConfig(), apiClient = useApiClient(config?.__customApiClient), { runInstruction: runInstructionRequest, loading } = useRunInstructionApi(apiClient), id = useId(), [inputs, setInputs] = useState(NO_INPUT), [runRequest, setRunRequest] = useState(), [resolveUserInput, setResolveUserInput] = useState(), getUserInput = useCallback(async ({ title, inputs: inputs2 }) => {
const userInputBlocks = inputs2.map((input, i) => ({
_type: userInputTypeName,
_key: input.id ?? `${i}`,
message: input.title,
description: input.description
}));
if (userInputBlocks.length)
return setRunRequest({ dialogTitle: title, userInputBlocks }), new Promise((resolve) => {
setResolveUserInput(() => resolve);
});
}, []), runInstruction = useCallback(
(req) => {
if (loading)
return;
const { instruction: instruction2, ...request } = req, instructionKey = instruction2._key, userInputBlocks = instruction2?.prompt?.flatMap(
(block) => block._type === "block" ? block.children.filter(isUserInputBlock) : [block]
).filter(isUserInputBlock);
if (!userInputBlocks?.length) {
runInstructionRequest({
...request,
instructionKey,
userTexts: void 0
});
return;
}
setRunRequest({
...req,
userInputBlocks
});
},
[runInstructionRequest, loading]
), close = useCallback(() => {
setRunRequest(void 0), setInputs(NO_INPUT), resolveUserInput && resolveUserInput(void 0), setResolveUserInput(void 0);
}, [resolveUserInput]), runWithInput = useCallback(() => {
if (runRequest)
if ("instruction" in runRequest) {
const { instruction: instruction2, userTexts, ...request } = runRequest;
runInstructionRequest({
...request,
instructionKey: instruction2._key,
userTexts: Object.entries(inputs).map(([key, value]) => ({
blockKey: key,
userInput: value
}))
});
} else {
const userInputs = Object.values(inputs).map((input, i) => {
const userInputBlock = runRequest.userInputBlocks[i];
return {
input: {
id: userInputBlock._key,
title: userInputBlock.message ?? "",
description: userInputBlock.description
},
result: input
};
});
resolveUserInput?.(userInputs), setResolveUserInput(void 0);
}
close();
}, [close, runInstructionRequest, runRequest, inputs, resolveUserInput]), open = !!runRequest, runDisabled = useMemo(
() => (runRequest?.userInputBlocks?.length ?? 0) > Object.entries(inputs).filter(([, value]) => !!value).length,
[runRequest?.userInputBlocks, inputs]
), runButton = /* @__PURE__ */ jsx(
Button,
{
text: "Run instruction",
onClick: runWithInput,
tone: "primary",
icon: PlayIcon,
style: { width: "100%" },
disabled: runDisabled
}
), contextValue = useMemo(
() => ({ runInstruction, getUserInput, instructionLoading: loading }),
[runInstruction, loading]
);
return /* @__PURE__ */ jsxs(RunInstructionContext.Provider, { value: contextValue, children: [
open ? /* @__PURE__ */ jsx(
Dialog,
{
id,
open,
onClose: close,
width: 1,
header: "dialogTitle" in runRequest ? runRequest.dialogTitle : getInstructionTitle(runRequest?.instruction),
footer: /* @__PURE__ */ jsx(Flex, { justify: "space-between", padding: 2, flex: 1, children: runDisabled ? /* @__PURE__ */ jsx(
Tooltip,
{
content: /* @__PURE__ */ jsx(Flex, { padding: 2, children: /* @__PURE__ */ jsx(Text, { children: "Unable to run instruction. All fields must have a value." }) }),
placement: "top",
children: /* @__PURE__ */ jsx(Flex, { flex: 1, children: runButton })
}
) : runButton }),
children: /* @__PURE__ */ jsx(Stack, { padding: 4, space: 2, children: runRequest?.userInputBlocks?.map((block, i) => /* @__PURE__ */ jsx(
UserInput,
{
block,
autoFocus: i === 0,
inputs,
setInputs
},
block._key
)) })
}
) : null,
props.children
] });
}
function UserInput(props) {
const { block, autoFocus, setInputs, inputs } = props, key = block._key, textAreaRef = useRef(null), onChange = useCallback(
(e) => {
setInputs((current) => ({
...current,
[key]: (e.currentTarget ?? e.target).value
}));
},
[key, setInputs]
), value = useMemo(() => inputs[key], [inputs, key]);
return useEffect(() => {
autoFocus && setTimeout(() => textAreaRef.current?.focus(), 0);
}, [autoFocus]), /* @__PURE__ */ jsxs(Stack, { padding: 2, space: 3, children: [
/* @__PURE__ */ jsx(
FormFieldHeaderText,
{
title: block?.message ?? "Provide more context",
description: block.description
}
),
/* @__PURE__ */ jsx(
TextArea,
{
ref: textAreaRef,
rows: 4,
value,
onChange,
style: { resize: "vertical" }
}
)
] });
}
function isDocAssistable(documentSchemaType, published, draft) {
return !!(documentSchemaType.liveEdit ? published : draft);
}
function useRequestRunInstruction(args) {
const { runInstruction, instructionLoading } = useRunInstruction(), requestRunInstruction = useDraftDelayedTask({
...args,
task: runInstruction
});
return {
instructionLoading,
requestRunInstruction
};
}
function useDraftDelayedTask(args) {
const { documentOnChange, isDocAssistable: isDocAssistable2, task } = args, [queuedArgs, setQueuedArgs] = useState(void 0);
return useEffect(() => {
queuedArgs && isDocAssistable2 && (task(queuedArgs), setQueuedArgs(void 0));
}, [queuedArgs, isDocAssistable2, task]), useCallback(
(taskArgs) => {
documentOnChange(PatchEvent.from([unset(["_force_document_creation"])])), setQueuedArgs(taskArgs);
},
[setQueuedArgs, documentOnChange]
);
}
function useAssistDocumentContextValue(documentId, documentType) {
const schema = useSchema(), documentSchemaType = useMemo(() => {
const schemaType = schema.get(documentType);
if (!schemaType)
throw new Error(`Schema type "${documentType}" not found`);
return schemaType;
}, [documentType, schema]), {
openInspector,
closeInspector,
inspector,
onChange: documentOnChange,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore this is a valid option available in `corel` - Remove after corel is merged to next
selectedReleaseId,
editState
} = useDocumentPane(), { draft, published, version } = editState || {}, assistableDocumentId = selectedReleaseId ? getVersionId(documentId, selectedReleaseId) : documentSchemaType.liveEdit ? documentId : getDraftId(documentId), documentIsNew = selectedReleaseId ? !version?._id : !draft?._id && !published?._id, documentIsAssistable = selectedReleaseId ? !!version : isDocAssistable(documentSchemaType, published, draft), { params } = useAiPaneRouter(), selectedPath = params[fieldPathParam], assistDocument = useStudioAssistDocument({
documentId: assistableDocumentId,
schemaType: documentSchemaType
}), { syntheticTasks, addSyntheticTask, removeSyntheticTask } = useSyntheticTasks(assistableDocumentId), fieldRefs = getFieldRefs(documentSchemaType), fieldRefsByTypePath = asFieldRefsByTypePath(fieldRefs);
return useMemo(() => {
const base = {
assistableDocumentId,
documentSchemaType,
documentIsNew,
documentIsAssistable,
openInspector,
closeInspector,
inspector,
documentOnChange,
selectedPath,
syntheticTasks,
addSyntheticTask,
removeSyntheticTask,
fieldRefs,
fieldRefsByTypePath
};
return assistDocument ? {
...base,
loading: !1,
assistDocument
} : { ...base, loading: !0, assistDocument: void 0 };
}, [
assistDocument,
documentIsAssistable,
assistableDocumentId,
documentSchemaType,
documentIsNew,
openInspector,
closeInspector,
inspector,
documentOnChange,
selectedPath,
syntheticTasks,
addSyntheticTask,
removeSyntheticTask
]);
}
function useSyntheticTasks(assistableDocumentId) {
const [syntheticTasks, setSyntheticTasks] = useState(() => []), addSyntheticTask = useCallback((task) => {
setSyntheticTasks((current) => [...current, task]);
}, []), removeSyntheticTask = useCallback((task) => {
setSyntheticTasks((current) => current.filter((t) => task._key !== t._key));
}, []);
return useEffect(() => {
setSyntheticTasks([]);
}, [assistableDocumentId]), {
syntheticTasks,
addSyntheticTask,
removeSyntheticTask
};
}
function AssistDocumentContextProvider(props) {
const { documentId, documentType } = props, value = useAssistDocumentContextValue(documentId, documentType);
return /* @__PURE__ */ jsx(AssistDocumentContext.Provider, { value, children: props.children });
}
function AssistDocumentLayout(props) {
const { documentId, documentType } = props;
return /* @__PURE__ */ jsx(AssistDocumentContextProvider, { documentType, documentId, children: props.renderDefault(props) });
}
function AssistFeatureBadge() {
return /* @__PURE__ */ jsx(Badge, { fontSize: 0, style: { margin: "-2px 0" }, tone: "primary", children: "Beta" });
}
function AssistOnboardingPopover(props) {
const { dismiss } = props;
return /* @__PURE__ */ jsx(
Popover,
{
content: /* @__PURE__ */ jsx(AssistIntroCard, { dismiss }),
open: !0,
portal: !0,
placeholder: "bottom",
tone: "default",
width: 0,
children: /* @__PURE__ */ jsx(Card, { radius: 2, shadow: 2, style: { padding: 2, lineHeight: 0 }, children: /* @__PURE__ */ jsx(Button, { disabled: !0, fontSize: 1, icon: SparklesIcon, mode: "bleed", padding: 2 }) })
}
);
}
function AssistIntroCard(props) {
const buttonRef = useRef(null);
return /* @__PURE__ */ jsxs(Stack, { as: "section", padding: 3, space: 3, children: [
/* @__PURE__ */ jsxs(Stack, { padding: 2, space: 4, children: [
/* @__PURE__ */ jsxs(Flex, { gap: 2, align: "center", children: [
/* @__PURE__ */ jsx(Text, { as: "h1", size: 1, weight: "semibold", children: pluginTitle }),
/* @__PURE__ */ jsx("div", { "aria-hidden": !0, style: { margin: "-3px 0", lineHeight: 0 }, children: /* @__PURE__ */ jsx(AssistFeatureBadge, {}) })
] }),
/* @__PURE__ */ jsx(Stack, { space: 3, children: /* @__PURE__ */ jsxs(Text, { as: "p", muted: !0, size: 1, children: [
"Manage reusable AI instructions to boost your content creation and reduce amount of repetitive chores.",
" ",
/* @__PURE__ */ jsxs("a", { href: releaseAnnouncementUrl, target: "_blank", rel: "noreferrer", children: [
"Learn more ",
/* @__PURE__ */ jsx(ArrowRightIcon, {})
] })
] }) })
] }),
/* @__PURE__ */ jsx(
Button,
{
fontSize: 1,
icon: CheckmarkIcon,
onClick: props.dismiss,
padding: 3,
ref: buttonRef,
text: "Ok, good to know!",
tone: "primary"
}
)
] });
}
const inspectorOnboardingKey = "sanityStudio:assist:inspector:onboarding:dismissed", fieldOnboardingKey = "sanityStudio:assist:field:onboarding:dismisse