UNPKG

@sanity/assist

Version:

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

344 lines (309 loc) 9.17 kB
import type {SanityClient} from '@sanity/client' import {useToast} from '@sanity/ui' import {useCallback, useMemo, useState} from 'react' import {Path, pathToString, useClient, useCurrentUser, useSchema} from 'sanity' import {useAiAssistanceConfig} from './assistLayout/AiAssistanceConfigContext' import {ConditionalMemberState} from './helpers/conditionalMembers' import {serializeSchema} from './schemas/serialize/serializeSchema' import {FieldLanguageMap} from './translate/paths' import {documentRootKey} from './types' export interface UserTextInstance { blockKey: string userInput: string } export interface RunInstructionRequest { documentId: string assistDocumentId: string path: string typePath?: string instructionKey: string userId?: string userTexts?: UserTextInstance[] conditionalMembers?: ConditionalMemberState[] } export interface InstructStatus { enabled: boolean initialized: boolean validToken: boolean } export interface TranslateRequest { documentId: string translatePath: Path languagePath?: string styleguide: () => Promise<string | undefined> fieldLanguageMap?: FieldLanguageMap[] conditionalMembers?: ConditionalMemberState[] } const basePath = '/assist/tasks/instruction' export const API_VERSION_WITH_EXTENDED_TYPES = '2025-04-01' export function canUseAssist(status: InstructStatus | undefined) { return status?.enabled && status.initialized && status.validToken } export function useApiClient(customApiClient?: (defaultClient: SanityClient) => SanityClient) { const client = useClient({apiVersion: API_VERSION_WITH_EXTENDED_TYPES}) return useMemo( () => (customApiClient ? customApiClient(client) : client), [client, customApiClient], ) } export function useTranslate(apiClient: SanityClient) { const [loading, setLoading] = useState(false) const user = useCurrentUser() const schema = useSchema() const types = useMemo(() => serializeSchema(schema, {leanFormat: true}), [schema]) const toast = useToast() const translate = useCallback( ({ documentId, languagePath, styleguide, translatePath, fieldLanguageMap, conditionalMembers, }: TranslateRequest) => { setLoading(true) 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) => { toast.push({ status: 'error', title: 'Translate failed', description: e.message, }) setLoading(false) throw e }) .finally(() => { // adding some artificial delay here // server responds with 201 then proceeds; we dont need to allow spamming the button setTimeout(() => { setLoading(false) }, 2000) }) }, [setLoading, apiClient, toast, user, types], ) return useMemo( () => ({ translate, loading, }), [translate, loading], ) } export function useGenerateCaption(apiClient: SanityClient) { const [loading, setLoading] = useState(false) const user = useCurrentUser() const schema = useSchema() const types = useMemo(() => serializeSchema(schema, {leanFormat: true}), [schema]) const toast = useToast() const generateCaption = useCallback( ({path, documentId}: {path: string; documentId: string}) => { setLoading(true) return 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) => { toast.push({ status: 'error', title: 'Generate image description failed', description: e.message, }) setLoading(false) throw e }) .finally(() => { // adding some artificial delay here // server responds with 201 then proceeds; we dont need to allow spamming the button setTimeout(() => { setLoading(false) }, 2000) }) }, [setLoading, apiClient, toast, user, types], ) return useMemo( () => ({ generateCaption, loading, }), [generateCaption, loading], ) } export function useGenerateImage(apiClient: SanityClient) { const [loading, setLoading] = useState(false) const user = useCurrentUser() const schema = useSchema() const types = useMemo(() => serializeSchema(schema, {leanFormat: true}), [schema]) const toast = useToast() const generateImage = useCallback( ({path, documentId}: {path: string; documentId: string}) => { setLoading(true) return 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) => { toast.push({ status: 'error', title: 'Generate image from prompt failed', description: e.message, }) setLoading(false) throw e }) .finally(() => { // adding some artificial delay here // server responds with 201 then proceeds; we dont need to allow spamming the button setTimeout(() => { setLoading(false) }, 2000) }) }, [setLoading, apiClient, toast, user, types], ) return useMemo( () => ({ generateImage, loading, }), [generateImage, loading], ) } export function useGetInstructStatus(apiClient: SanityClient) { const [loading, setLoading] = useState(true) const getInstructStatus = useCallback(async () => { setLoading(true) const projectId = apiClient.config().projectId try { const status = await apiClient.request<InstructStatus>({ method: 'GET', url: `${basePath}/${apiClient.config().dataset}/status?projectId=${projectId}`, }) return status } finally { setLoading(false) } }, [setLoading, apiClient]) return { loading, getInstructStatus, } } export function useInitInstruct(apiClient: SanityClient) { const [loading, setLoading] = useState(false) const initInstruct = useCallback(() => { setLoading(true) return apiClient .request({ method: 'GET', url: `${basePath}/${apiClient.config().dataset}/init?projectId=${ apiClient.config().projectId }`, }) .finally(() => { setLoading(false) }) }, [setLoading, apiClient]) return { loading, initInstruct, } } export function useRunInstructionApi(apiClient: SanityClient) { const toast = useToast() const [loading, setLoading] = useState(false) const user = useCurrentUser() const schema = useSchema() const types = useMemo(() => serializeSchema(schema, {leanFormat: true}), [schema]) const { config: {assist: assistConfig}, } = useAiAssistanceConfig() const runInstruction = useCallback( (request: RunInstructionRequest) => { if (!user) { toast.push({ status: 'error', title: 'Unable to get user for instruction.', }) return undefined } setLoading(true) const {timeZone, locale} = Intl.DateTimeFormat().resolvedOptions() const defaultLocaleSettings = {timeZone, locale} const 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) => { toast.push({ status: 'error', title: 'Instruction failed', description: e.message, }) throw e }) .finally(() => { setLoading(false) }) }, [apiClient, types, user, toast, assistConfig], ) return useMemo( () => ({ runInstruction, loading, }), [runInstruction, loading], ) }