UNPKG

@sanity/assist

Version:

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

1,162 lines (1,159 loc) 180 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: !0 }); var jsxRuntime = require("react/jsx-runtime"), sanity = require("sanity"), ui = require("@sanity/ui"), react = require("react"), structure = require("sanity/structure"), dateFns = require("date-fns"), icons = require("@sanity/icons"), mutator = require("@sanity/mutator"), styledComponents = require("styled-components"), operators = require("rxjs/operators"), get = require("lodash/get.js"), isEqual = require("react-fast-compare"), rxjs = require("rxjs"), rxjsExhaustmapWithTrailing = require("rxjs-exhaustmap-with-trailing"); function _interopDefaultCompat(e) { return e && typeof e == "object" && "default" in e ? e : { default: e }; } var get__default = /* @__PURE__ */ _interopDefaultCompat(get), isEqual__default = /* @__PURE__ */ _interopDefaultCompat(isEqual); function hasOverflowScroll(el) { const overflow = getComputedStyle(el).overflow; return overflow.includes("auto") || overflow.includes("hidden") || overflow.includes("scroll"); } function useRegionRects() { const ref = react.useRef(null), [relativeBoundsRect, setRelativeBoundsRect] = react.useState(null), [relativeElementRect, setRelativeElementRect] = react.useState(null), [boundsScroll, setBoundsScroll] = react.useState({ x: 0, y: 0 }), [scroll, setScroll] = react.useState({ x: 0, y: 0 }), boundsScrollXRef = react.useRef(0), boundsScrollYRef = react.useRef(0), elementScrollXRef = react.useRef(0), elementScrollYRef = react.useRef(0); react.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 = react.useMemo( () => relativeBoundsRect && { x: relativeBoundsRect.x - boundsScroll.x, y: relativeBoundsRect.y - boundsScroll.y, w: relativeBoundsRect.w, h: relativeBoundsRect.h }, [relativeBoundsRect, boundsScroll] ), element = react.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 react.useEffect(() => { onRectsChange?.(bounds && element ? { bounds, element } : null); }, [bounds, element, onRectsChange]), /* @__PURE__ */ jsxRuntime.jsx("div", { ...restProps, ref, children }); } const ConnectorsStoreContext = react.createContext(null); function useConnectorsStore() { const store = react.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] = react.useState(null); return react.useEffect(() => store.from.subscribe(key, { zIndex }), [key, store, zIndex]), react.useEffect(() => { rects && store.from.next(key, rects); }, [key, rects, store]), /* @__PURE__ */ jsxRuntime.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 = react.useMemo(() => createConnectorsStore(), []); return react.useEffect( () => onConnectorsChange && store.connectors.subscribe(onConnectorsChange), [onConnectorsChange, store] ), /* @__PURE__ */ jsxRuntime.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 react.useMemo(() => getPathKey(path), [path]); } function getPathKey(path) { return path.length ? Array.isArray(path) ? sanity.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 = react.createContext(void 0); function FirstAssistedPathProvider(props) { const { members } = props, firstAssistedPath = react.useMemo(() => { const firstAssisted = members.find( (member) => member.kind === "field" && isAssistSupported(member.field.schemaType) ); return firstAssisted?.field.path ? sanity.pathToString(firstAssisted?.field.path) : void 0; }, [members]); return /* @__PURE__ */ jsxRuntime.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 = dateFns.minutesToMilliseconds(30), illegalIdChars = /[^a-zA-Z0-9._-]/g; function assistDocumentId(documentType) { return `${assistDocumentIdPrefix}${documentType}`.replace(illegalIdChars, "_"); } function assistTasksStatusId(documentId) { return sanity.isVersionId(documentId) ? `${assistDocumentStatusIdPrefix}${sanity.getVersionFromId(documentId)}.${sanity.getPublishedId(documentId)}` : `${assistDocumentStatusIdPrefix}${sanity.getPublishedId(documentId)}`; } function useDocumentState(id, docType) { const state = sanity.useEditState(id, docType); return state.draft || state.published; } function useStudioAssistDocument({ documentId, schemaType, initDoc }) { const documentTypeName = schemaType.name, currentUser = sanity.useCurrentUser(), assistDocument = useDocumentState( assistDocumentId(documentTypeName), assistDocumentTypeName ), assistTasksStatus = useDocumentState( assistTasksStatusId(documentId ?? ""), assistTasksStatusTypeName ), client = sanity.useClient({ apiVersion: "2023-01-01" }); return react.useEffect(() => { !assistDocument && initDoc && client.createIfNotExists({ _id: assistDocumentId(documentTypeName), _type: assistDocumentTypeName }).catch(() => { }); }, [client, assistDocument, documentTypeName, initDoc]), react.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 sanity.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 = sanity.useCurrentUser(), toast = ui.useToast(), tasks = assistDocument?.tasks, previousTasks = react.useRef("initial"); react.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 && dateFns.isAfter(dateFns.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__ */ jsxRuntime.jsx(AssistInput, { ...props }); const documentId = props.value?._id; return documentId ? /* @__PURE__ */ jsxRuntime.jsx(AssistDocumentInput, { ...props, documentId }) : props.renderDefault(props); } function AssistDocumentInput({ documentId, ...props }) { useInstructionToaster(documentId, props.schemaType); const schemaType = react.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__ */ jsxRuntime.jsx(FirstAssistedPathProvider, { members: props.members, children: props.renderDefault({ ...props, schemaType }) }); } function AssistInput(props) { const { zIndex } = ui.useLayer(), { paneKey } = structure.useDocumentPane(), pathKey = usePathKey(props.path); return /* @__PURE__ */ jsxRuntime.jsx(ConnectFromRegion, { _key: `${paneKey}_${pathKey}`, zIndex, style: { minWidth: 0 }, children: props.renderDefault(props) }); } const AssistDocumentContext = react.createContext( void 0 ); function useAssistDocumentContext() { const context = react.useContext(AssistDocumentContext); if (!context) throw new Error("AssistDocumentContext value is missing"); return context; } const AiAssistanceConfigContext = react.createContext({}); function useAiAssistanceConfig() { const context = react.useContext(AiAssistanceConfigContext); if (!context) throw new Error("Missing AiAssistanceConfigContext"); return context; } function useSerializedTypes() { return useAiAssistanceConfig().serializedTypes; } 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 = sanity.useClient({ apiVersion: API_VERSION_WITH_EXTENDED_TYPES }); return react.useMemo( () => customApiClient ? customApiClient(client) : client, [client, customApiClient] ); } function useTranslate(apiClient) { const [loading, setLoading] = react.useState(!1), user = sanity.useCurrentUser(), types = useSerializedTypes(), toast = ui.useToast(), translate = react.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 : sanity.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 react.useMemo( () => ({ translate, loading }), [translate, loading] ); } function useGenerateCaption(apiClient) { const [loading, setLoading] = react.useState(!1), user = sanity.useCurrentUser(), types = useSerializedTypes(), toast = ui.useToast(), generateCaption = react.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 react.useMemo( () => ({ generateCaption, loading }), [generateCaption, loading] ); } function useGenerateImage(apiClient) { const [loading, setLoading] = react.useState(!1), user = sanity.useCurrentUser(), types = useSerializedTypes(), toast = ui.useToast(), generateImage = react.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 react.useMemo( () => ({ generateImage, loading }), [generateImage, loading] ); } function useGetInstructStatus(apiClient) { const [loading, setLoading] = react.useState(!0), getInstructStatus = react.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] = react.useState(!1), initInstruct = react.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 = ui.useToast(), [loading, setLoading] = react.useState(!1), user = sanity.useCurrentUser(), types = useSerializedTypes(), { config: { assist: assistConfig } } = useAiAssistanceConfig(), runInstruction = react.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 react.useMemo( () => ({ runInstruction, loading }), [runInstruction, loading] ); } const NO_INPUT = {}, RunInstructionContext = react.createContext({ runInstruction: () => { }, getUserInput: async () => { }, instructionLoading: !1 }); function useRunInstruction() { return react.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 = react.useId(), [inputs, setInputs] = react.useState(NO_INPUT), [runRequest, setRunRequest] = react.useState(), [resolveUserInput, setResolveUserInput] = react.useState(), getUserInput = react.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 = react.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 = react.useCallback(() => { setRunRequest(void 0), setInputs(NO_INPUT), resolveUserInput && resolveUserInput(void 0), setResolveUserInput(void 0); }, [resolveUserInput]), runWithInput = react.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 = react.useMemo( () => (runRequest?.userInputBlocks?.length ?? 0) > Object.entries(inputs).filter(([, value]) => !!value).length, [runRequest?.userInputBlocks, inputs] ), runButton = /* @__PURE__ */ jsxRuntime.jsx( ui.Button, { text: "Run instruction", onClick: runWithInput, tone: "primary", icon: icons.PlayIcon, style: { width: "100%" }, disabled: runDisabled } ), contextValue = react.useMemo( () => ({ runInstruction, getUserInput, instructionLoading: loading }), [runInstruction, loading] ); return /* @__PURE__ */ jsxRuntime.jsxs(RunInstructionContext.Provider, { value: contextValue, children: [ open ? /* @__PURE__ */ jsxRuntime.jsx( ui.Dialog, { id, open, onClose: close, width: 1, header: "dialogTitle" in runRequest ? runRequest.dialogTitle : getInstructionTitle(runRequest?.instruction), footer: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { justify: "space-between", padding: 2, flex: 1, children: runDisabled ? /* @__PURE__ */ jsxRuntime.jsx( ui.Tooltip, { content: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { padding: 2, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { children: "Unable to run instruction. All fields must have a value." }) }), placement: "top", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { flex: 1, children: runButton }) } ) : runButton }), children: /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { padding: 4, space: 2, children: runRequest?.userInputBlocks?.map((block, i) => /* @__PURE__ */ jsxRuntime.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 = react.useRef(null), onChange = react.useCallback( (e) => { setInputs((current) => ({ ...current, [key]: (e.currentTarget ?? e.target).value })); }, [key, setInputs] ), value = react.useMemo(() => inputs[key], [inputs, key]); return react.useEffect(() => { autoFocus && setTimeout(() => textAreaRef.current?.focus(), 0); }, [autoFocus]), /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 2, space: 3, children: [ /* @__PURE__ */ jsxRuntime.jsx( sanity.FormFieldHeaderText, { title: block?.message ?? "Provide more context", description: block.description } ), /* @__PURE__ */ jsxRuntime.jsx( ui.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] = react.useState(void 0); return react.useEffect(() => { queuedArgs && isDocAssistable2 && (task(queuedArgs), setQueuedArgs(void 0)); }, [queuedArgs, isDocAssistable2, task]), react.useCallback( (taskArgs) => { documentOnChange(sanity.PatchEvent.from([sanity.unset(["_force_document_creation"])])), setQueuedArgs(taskArgs); }, [setQueuedArgs, documentOnChange] ); } const maxDepth = 6; function getTypeIcon(schemaType) { let t = schemaType; for (; t; ) { if (t.icon) return t.icon; t = t.type; } return isType(schemaType, "slug") ? icons.LinkIcon : isType(schemaType, "image") ? icons.ImageIcon : schemaType.jsonType === "array" && isPortableTextArray(schemaType) ? icons.BlockContentIcon : schemaType.jsonType === "array" ? icons.OlistIcon : schemaType.jsonType === "object" ? icons.BlockquoteIcon : schemaType.jsonType === "string" ? icons.StringIcon : icons.DocumentIcon; } function asFieldRefsByTypePath(fieldRefs) { return fieldRefs.reduce( (acc, ref) => ({ ...acc, [ref.key]: ref }), {} ); } function getDocumentFieldRef(schemaType) { return { key: documentRootKey, icon: schemaType.icon ?? icons.DocumentIcon, title: "The entire document", path: [], schemaType }; } 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(sanity.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(sanity.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 = sanity.stringToPath(pathString), currentPath = []; let valid = !0; const syntheticPath = path.map((segment) => { if (currentPath.push(segment), sanity.isKeySegment(segment)) { const match = mutator.extractWithPath(sanity.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(sanity.pathToString(syntheticPath)) : void 0; } function patchableKey(pathKey) { return pathKey.replace(/[=]=/g, ":").replace(/[[\]]/g, "|").replace(/"/g, ""); } function useTypePath(doc, pathString) { return react.useMemo(() => getTypePath(doc, pathString), [doc, pathString]); } function useSelectedField(documentSchemaType, path) { const { getFieldRefs: getFieldRefs2 } = useAiAssistanceConfig(), selectableFields = react.useMemo( () => documentSchemaType && sanity.isObjectSchemaType(documentSchemaType) ? [getDocumentFieldRef(documentSchemaType), ...getFieldRefs2(documentSchemaType.name)] : [], [documentSchemaType, getFieldRefs2] ); return react.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 = structure.usePaneRouter(); return react.useMemo( () => ({ ...paneRouter, params: paneRouter.params ?? {} }), [paneRouter] ); } function useAssistDocumentContextValue(documentId, documentType) { const schema = sanity.useSchema(), { getFieldRefs: getFieldRefs2, getFieldRefsByTypePath } = useAiAssistanceConfig(), documentSchemaType = react.useMemo(() => { const schemaType = schema.get(documentType); if (!schemaType) throw new Error(`Schema type "${documentType}" not found`); return schemaType; }, [documentType, schema]), { fieldRefs, fieldRefsByTypePath } = react.useMemo(() => ({ fieldRefs: getFieldRefs2(documentType), fieldRefsByTypePath: getFieldRefsByTypePath(documentType) }), [getFieldRefs2, getFieldRefsByTypePath, documentType]), { 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 } = structure.useDocumentPane(), { draft, published, version } = editState || {}, assistableDocumentId = selectedReleaseId ? sanity.getVersionId(documentId, selectedReleaseId) : documentSchemaType.liveEdit ? documentId : sanity.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); return react.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, fieldRefs, fieldRefsByTypePath ]); } function useSyntheticTasks(assistableDocumentId) { const [syntheticTasks, setSyntheticTasks] = react.useState(() => []), addSyntheticTask = react.useCallback((task) => { setSyntheticTasks((current) => [...current, task]); }, []), removeSyntheticTask = react.useCallback((task) => { setSyntheticTasks((current) => current.filter((t) => task._key !== t._key)); }, []); return react.useEffect(() => { setSyntheticTasks([]); }, [assistableDocumentId]), { syntheticTasks, addSyntheticTask, removeSyntheticTask }; } function AssistDocumentContextProvider(props) { const { documentId, documentType } = props, value = useAssistDocumentContextValue(documentId, documentType); return /* @__PURE__ */ jsxRuntime.jsx(AssistDocumentContext.Provider, { value, children: props.children }); } function AssistDocumentLayout(props) { const { documentId, documentType } = props; return /* @__PURE__ */ jsxRuntime.jsx(AssistDocumentContextProvider, { documentType, documentId, children: props.renderDefault(props) }); } function AssistFeatureBadge() { return /* @__PURE__ */ jsxRuntime.jsx(ui.Badge, { fontSize: 0, style: { margin: "-2px 0" }, tone: "primary", children: "Beta" }); } function AssistOnboardingPopover(props) { const { dismiss } = props; return /* @__PURE__ */ jsxRuntime.jsx( ui.Popover, { content: /* @__PURE__ */ jsxRuntime.jsx(AssistIntroCard, { dismiss }), open: !0, portal: !0, placeholder: "bottom", tone: "default", width: 0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { radius: 2, shadow: 2, style: { padding: 2, lineHeight: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Button, { disabled: !0, fontSize: 1, icon: icons.SparklesIcon, mode: "bleed", padding: 2 }) }) } ); } function AssistIntroCard(props) { const buttonRef = react.useRef(null); return /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { as: "section", padding: 3, space: 3, children: [ /* @__PURE__ */ jsxRuntime.jsxs(ui.Stack, { padding: 2, space: 4, children: [ /* @__PURE__ */ jsxRuntime.jsxs(ui.Flex, { gap: 2, align: "center", children: [ /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "h1", size: 1, weight: "semibold", children: pluginTitle }), /* @__PURE__ */ jsxRuntime.jsx("div", { "aria-hidden": !0, style: { margin: "-3px 0", lineHeight: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(AssistFeatureBadge, {}) }) ] }), /* @__PURE__ */ jsxRuntime.jsx(ui.Stack, { space: 3, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Text, { as: "p", muted: !0, size: 1, children: [ "Manage reusable AI instructions to boost your content creation and reduce amount of repetitive chores.", " ", /* @__PURE__ */ jsxRuntime.jsxs("a", { href: releaseAnnouncementUrl, target: "_blank", rel: "noreferrer", children: [ "Learn more ", /* @__PURE__ */ jsxRuntime.jsx(icons.ArrowRightIcon, {}) ] }) ] }) }) ] }), /* @__PURE__ */ jsxRuntime.jsx( ui.Button, { fontSize: 1, icon: icons.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:dismissed"; function isFeatureOnboardingDismissed(featureKey) { return typeof localStorage > "u" ? !1 : localStorage.getItem(featureKey) === "true"; } function dismissFeatureOnboarding(featureKey) { typeof localStorage > "u" || localStorage.setItem(featureKey, "true"); } function useOnboardingFeature(featureKey) { const [showOnboarding, setShowOnboarding] = react.useState( () => !isFeatureOnboardingDismissed(featureKey) ), dismissOnboarding = react.useCallback(() => { setShowOnboarding(!1), dismissFeatureOnboarding(featureKey); }, [setShowOnboarding, featureKey]); return { showOnboarding, dismissOnboarding }; } const fadeIn = styledComponents.keyframes` 0% { opacity: 0; transform: scale(0.75); } 40% { opacity: 0; transform: scale(0.75); } 100% { opacity: 1; transform: scale(1); } `, FadeInDiv = styledComponents.styled.div` animation-name: ${fadeIn}; animation-timing-function: ease-in-out; `, FadeInContent = react.forwardRef(function({ children, durationMs = 250 }, ref) { return /* @__PURE__ */ jsxRuntime.jsx(FadeInDiv, { ref, style: { animationDuration: `${durationMs}ms` }, children }); }), purple = { 400: { hex: "#b087f7" }, 500: { hex: "#8f57ef" }, 600: { hex: "#721fe5" } }, Root = styledComponents.styled.span` display: block; width: 25px; height: 25px; position: relative; `, dash = styledComponents.keyframes` 0% { transform: rotate(0); } 100% { transform: rotate(43deg); } `, Outline = styledComponents.styled.svg` display: block; position: absolute; top: 0; left: 0; & > circle { stroke: var(--ai-avatar-stroke-color); stroke-width: 1.5px; stroke-linecap: round; transform-origin: center; animation: ${dash} 500ms ease-in-out infinite; transition: stroke-dasharray 200ms ease-in-out; stroke-dasharray: 2.34px 0; [data-state='active'] > & { stroke-dasharray: 2px 2.34px; } } `, IconDisc = styledComponents.styled.span` background: var(--ai-avatar-disc-color); color: white; width: 21px; height: 21px; display: flex; align-items: center; justify-content: center; border-radius: 10.5px; position: absolute; top: 2px; left: 2px; `; function AssistAvatar(props) { const { state = "present" } = props, scheme = sanity.useColorSchemeValue(), style = react.useMemo(() => scheme === "dark" ? { "--ai-avatar-stroke-color": purple[400].hex, "--ai-avatar-disc-color": purple[600].hex } : { "--ai-avatar-stroke-color": purple[500].hex, "--ai-avatar-disc-color": purple[600].hex }, [scheme]); return /* @__PURE__ */ jsxRuntime.jsxs(Root, { "data-state": state, style, children: [ /* @__PURE__ */ jsxRuntime.jsx( Outline, { width: "25", height: "25", viewBox: "0 0 25 25", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12.5", cy: "12.5", r: "11.75" }) } ), /* @__PURE__ */ jsxRuntime.jsx(IconDisc, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { as: "span", size: 0, style: { color: "inherit" }, children: /* @__PURE__ */ jsxRuntime.jsx(icons.SparklesIcon, { style: { color: "inherit" } }) }) }) ] }); } function AiFieldPresence(props) { return /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { style: { position: "relative", background: "transparent" }, contentEditable: !1, children: /* @__PURE__ */ jsxRuntime.jsx( ui.Tooltip, { placement: "left", content: /* @__PURE__ */ jsxRuntime.jsx(ui.Card, { padding: 3, border: !0, children: /* @__PURE__ */ jsxRuntime.jsx(ui.Flex, { align: "center", children: /* @__PURE__ */ jsxRuntime.jsx(ui.Text, { size: 1, children: "Running instruction..." }) }) }), children: /* @__PURE__ */ jsxRuntime.jsx(FadeInContent, { durationMs: 300, children: /* @__PURE__ */ jsxRuntime.jsx(AssistAvatar, { state: "active" }) }) } ) }); } const NO_PRESENCE = []; function useAssistPresence(path, showFocusWithin) { const context = useAssistDocumentContext(), tasks = (context && "assistDocument" in context ? context.assistDocument : void 0)?.tasks; return react.useMemo(() => { const activePresence = tasks?.filter((task) => !task.ended)?.flatMap((task) => task.presence ?? [])?.filter( (p) => p.started && (/* @__PURE__ */ new Date()).getTime() - new Date(p.started).getTime() < maxHistoryVisibilityMs ).filter((presence) => { if (!presence.path || !path.length) return !1; const statusPath = sanity.stringToPath(presence.path); return !showFocusWithin && statusPath.length !== path.length ? !1 : path.every((pathSegment, i) => { const statusSegment = statusPath[i]; return typeof pathSegment == "string" ? pathSegment === statusSegment : sanity.isKeySegment(pathSegment) && sanity.isKeySegment(statusSegment) ? pathSegment._key === statusSegment._key : !1; }); }); return activePresence?.length ? activePresence.map((status) => aiPresence(status, path)) : NO_PRESENCE; }, [showFocusWithin, tasks, path]); } function aiPresence(presence, path, title) { return { user: { id: `sanity-assistant_${presence._key}`, displayName: pluginTitle }, path, sessionId: "not-available", lastActiveAt: presence?.started ?? (/* @__PURE__ */ new Date()).toISOString() }; } function AssistFieldWrapper(props) { const { schemaType } = props; return !react.useMemo(() => isAssistSupported(schemaType), [schemaType]) || schemaType.name.startsWith("sanity.") || schemaType.name === contextDocumentTypeName ? props.renderDefault(props) : !isType(props.schemaType, "document") && props.inputId !== "root" && props.inputId !== assistFormId ? /* @__PURE__ */ jsxRuntime.jsx(AssistField, { ...props, children: props.children }) : props.renderDefault(props); } function AssistField(props) { const { path } = props, isPortableText = react.useMemo( () => !!(sanity.isArraySchemaType(props.schemaType) && isPortableTextArray(props.schemaType)), [props.schemaType] ), presence = useAssistPresence(props.path, isPortableText), firstAssistedPath = react.useContext(FirstAssistedPathContext), isFirstAssisted = react.useMemo( () => sanity.pathToString(path) === firstAssistedPath, [path, firstAssistedPath] ), { showOnboarding, dismissOnboarding } = useOnboardingFeature(fi