UNPKG

@sanity/assist

Version:

You create the instructions; Sanity AI Assist does the rest.

1,149 lines 171 kB
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