@sanity/assist 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/assist",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "You create the instructions; Sanity AI Assist does the rest.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -70,7 +70,7 @@
70
70
  "@rollup/plugin-image": "^3.0.3",
71
71
  "@sanity/pkg-utils": "^2.4.10",
72
72
  "@sanity/plugin-kit": "^3.1.10",
73
- "@sanity/semantic-release-preset": "^4.1.6",
73
+ "@sanity/semantic-release-preset": "^4.1.7",
74
74
  "@types/react": "^18.2.37",
75
75
  "@types/styled-components": "^5.1.30",
76
76
  "@typescript-eslint/eslint-plugin": "^7.0.1",
@@ -87,7 +87,7 @@
87
87
  "react-dom": "^18.2.0",
88
88
  "rimraf": "^5.0.5",
89
89
  "sanity": "^3.28.0",
90
- "semantic-release": "^21.1.2",
90
+ "semantic-release": "^23.0.2",
91
91
  "styled-components": "^6.1.1",
92
92
  "typescript": "^5.3.3",
93
93
  "vitest": "^1.2.1"
@@ -6,6 +6,7 @@ import {
6
6
  documentRootKey,
7
7
  fieldPathParam,
8
8
  instructionParam,
9
+ StudioInstruction,
9
10
  } from '../../types'
10
11
  import {createContext, useContext, useEffect, useMemo, useRef} from 'react'
11
12
  import {
@@ -16,6 +17,7 @@ import {
16
17
  KeyedSegment,
17
18
  ObjectInputProps,
18
19
  ObjectSchemaType,
20
+ PatchEvent,
19
21
  Path,
20
22
  SchemaType,
21
23
  set,
@@ -30,6 +32,7 @@ import {useAiPaneRouter} from '../../assistInspector/helpers'
30
32
  import {SelectedFieldContextProvider, SelectedFieldContextValue} from './SelectedFieldContext'
31
33
  import {Card, Stack, Text} from '@sanity/ui'
32
34
  import {documentTypeFromAiDocumentId} from '../../helpers/ids'
35
+ import {useAiAssistanceConfig} from '../../assistLayout/AiAssistanceConfigContext'
33
36
 
34
37
  const EMPTY_FIELDS: AssistField[] = []
35
38
 
@@ -104,8 +107,6 @@ function AssistDocumentFormEditable(props: ObjectInputProps) {
104
107
  }
105
108
  }, [title, documentSchema, onChange, id])
106
109
 
107
- const fieldExists = !!fields?.some((f) => f._key === typePath)
108
-
109
110
  const {onPathOpen, ...formCallbacks} = useFormCallbacks()
110
111
 
111
112
  const newCallbacks: FormCallbacksValue = useMemo(
@@ -141,7 +142,8 @@ function AssistDocumentFormEditable(props: ObjectInputProps) {
141
142
  key={typePath}
142
143
  pathKey={typePath}
143
144
  activePath={activePath}
144
- fieldExists={fieldExists}
145
+ fields={fields}
146
+ documentSchema={documentSchema}
145
147
  onChange={onChange}
146
148
  />
147
149
  {instruction && <BackToInstructionListLink />}
@@ -195,36 +197,78 @@ function useSelectedSchema(
195
197
  function FieldsInitializer({
196
198
  pathKey,
197
199
  activePath,
198
- fieldExists,
200
+ fields,
201
+ documentSchema,
199
202
  onChange,
200
203
  }: {
201
204
  pathKey?: string
202
205
  activePath?: Path
203
- fieldExists: boolean
206
+ fields: AssistField[] | undefined
207
+ documentSchema: ObjectSchemaType | undefined
204
208
  onChange: ObjectInputProps['onChange']
205
209
  }) {
210
+ const {
211
+ config: {__presets: presets},
212
+ } = useAiAssistanceConfig()
213
+
214
+ const existingField = fields?.find((f) => f._key === pathKey)
215
+ const documentPresets = !!documentSchema?.name && presets?.[documentSchema?.name]
216
+
217
+ const missingPresetInstructions = useMemo(() => {
218
+ if (!documentPresets || !pathKey) {
219
+ return undefined
220
+ }
221
+ const existingInstructions = existingField?.instructions
222
+ const presetField = documentPresets.fields?.find((f) => f.path === pathKey)
223
+ return presetField?.instructions?.filter(
224
+ (i) => !existingInstructions?.some((ei) => ei._key === i._key)
225
+ )
226
+ }, [documentPresets, pathKey, existingField])
227
+
206
228
  // need this to not fire onChange twice in React strict mode
207
229
  const initialized = useRef(false)
208
230
  useEffect(() => {
209
- if (initialized.current || fieldExists || activePath || !pathKey) {
231
+ if (initialized.current || !pathKey) {
232
+ return
233
+ }
234
+ if (existingField && !missingPresetInstructions?.length) {
210
235
  return
211
236
  }
212
- onChange([
213
- setIfMissing([], ['fields']),
214
- insert(
215
- [
216
- typed<AssistField>({
217
- _key: pathKey,
218
- _type: assistFieldTypeName,
219
- path: pathKey,
220
- }),
221
- ],
222
- 'after',
223
- ['fields', -1]
224
- ),
225
- ])
237
+
238
+ let event = PatchEvent.from([setIfMissing([], ['fields'])])
239
+ if (!existingField) {
240
+ event = event.append(
241
+ insert(
242
+ [
243
+ typed<AssistField>({
244
+ _key: pathKey,
245
+ _type: assistFieldTypeName,
246
+ path: pathKey,
247
+ }),
248
+ ],
249
+ 'after',
250
+ ['fields', -1]
251
+ )
252
+ )
253
+ }
254
+ if (missingPresetInstructions?.length) {
255
+ event = event.append(
256
+ insert(
257
+ missingPresetInstructions.map(
258
+ (preset): StudioInstruction => ({
259
+ ...preset,
260
+ _type: 'sanity.assist.instruction',
261
+ prompt: preset.prompt?.map((p) => ({markDefs: [], ...p})),
262
+ })
263
+ ),
264
+ 'after',
265
+ ['fields', {_key: pathKey}, 'instructions', -1]
266
+ )
267
+ )
268
+ }
269
+ onChange(event)
226
270
  initialized.current = true
227
- }, [activePath, onChange, pathKey, fieldExists])
271
+ }, [activePath, onChange, pathKey, existingField, missingPresetInstructions])
228
272
 
229
273
  return null
230
274
  }
@@ -2,7 +2,7 @@ import {createContext, useEffect, useMemo, useState} from 'react'
2
2
  import {InputProps, pathToString, useSyncState} from 'sanity'
3
3
  import {getDescriptionFieldOption, getImageInstructionFieldOption} from '../helpers/typeUtils'
4
4
  import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
5
- import {useApiClient, useGenerateCaption} from '../useApiClient'
5
+ import {canUseAssist, useApiClient, useGenerateCaption} from '../useApiClient'
6
6
  import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
7
7
  import {publicId} from '../helpers/ids'
8
8
 
@@ -20,7 +20,7 @@ export function ImageContextProvider(props: InputProps) {
20
20
  const [assetRefState, setAssetRefState] = useState<string | undefined>(assetRef)
21
21
 
22
22
  const {documentId, documentSchemaType} = useAssistDocumentContext()
23
- const {config} = useAiAssistanceConfig()
23
+ const {config, status} = useAiAssistanceConfig()
24
24
  const apiClient = useApiClient(config?.__customApiClient)
25
25
  const {generateCaption} = useGenerateCaption(apiClient)
26
26
 
@@ -28,11 +28,18 @@ export function ImageContextProvider(props: InputProps) {
28
28
 
29
29
  useEffect(() => {
30
30
  const descriptionField = getDescriptionFieldOption(schemaType)
31
- if (assetRef && documentId && descriptionField && assetRef !== assetRefState && !isSyncing) {
31
+ if (
32
+ assetRef &&
33
+ documentId &&
34
+ descriptionField &&
35
+ assetRef !== assetRefState &&
36
+ !isSyncing &&
37
+ canUseAssist(status)
38
+ ) {
32
39
  setAssetRefState(assetRef)
33
40
  generateCaption({path: pathToString([...path, descriptionField]), documentId: documentId})
34
41
  }
35
- }, [schemaType, path, assetRef, assetRefState, documentId, generateCaption, isSyncing])
42
+ }, [schemaType, path, assetRef, assetRefState, documentId, generateCaption, isSyncing, status])
36
43
 
37
44
  const context: ImageContextValue = useMemo(() => {
38
45
  const descriptionField = getDescriptionFieldOption(schemaType)
@@ -2,11 +2,14 @@ import {DocumentFieldAction, DocumentFieldActionGroup, DocumentFieldActionItem}
2
2
  import {ImageIcon} from '@sanity/icons'
3
3
  import {useContext, useMemo} from 'react'
4
4
  import {usePathKey} from '../helpers/misc'
5
- import {useApiClient, useGenerateCaption} from '../useApiClient'
5
+ import {canUseAssist, useApiClient, useGenerateCaption} from '../useApiClient'
6
6
  import {useAiAssistanceConfig} from '../assistLayout/AiAssistanceConfigContext'
7
7
  import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
8
8
  import {ImageContext} from '../components/ImageContext'
9
9
  import {Box, Spinner} from '@sanity/ui'
10
+ import {useDocumentPane} from 'sanity/desk'
11
+ import {aiInspectorId} from '../assistInspector/constants'
12
+ import {fieldPathParam, instructionParam} from '../types'
10
13
 
11
14
  function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
12
15
  return node
@@ -16,11 +19,11 @@ export const generateCaptionsActions: DocumentFieldAction = {
16
19
  name: 'sanity-assist-generate-captions',
17
20
  useAction(props) {
18
21
  const pathKey = usePathKey(props.path)
22
+ const {openInspector} = useDocumentPane()
19
23
 
20
- const {config} = useAiAssistanceConfig()
24
+ const {config, status} = useAiAssistanceConfig()
21
25
  const apiClient = useApiClient(config?.__customApiClient)
22
26
  const {generateCaption, loading} = useGenerateCaption(apiClient)
23
-
24
27
  const imageContext = useContext(ImageContext)
25
28
 
26
29
  if (imageContext && pathKey === imageContext?.imageDescriptionPath) {
@@ -43,13 +46,21 @@ export const generateCaptionsActions: DocumentFieldAction = {
43
46
  if (loading) {
44
47
  return
45
48
  }
49
+ if (!canUseAssist(status)) {
50
+ openInspector(aiInspectorId, {
51
+ [fieldPathParam]: pathKey,
52
+ [instructionParam]: undefined as any,
53
+ })
54
+ return
55
+ }
56
+
46
57
  generateCaption({path: pathKey, documentId: documentId ?? ''})
47
58
  },
48
59
  renderAsButton: true,
49
60
  disabled: loading,
50
61
  hidden: !imageContext.assetRef,
51
62
  })
52
- }, [generateCaption, pathKey, documentId, loading, imageContext])
63
+ }, [generateCaption, pathKey, documentId, loading, imageContext, status, openInspector])
53
64
  }
54
65
 
55
66
  // works but not supported by types
@@ -4,13 +4,17 @@ import {useMemo} from 'react'
4
4
 
5
5
  export function usePathKey(path: Path | string) {
6
6
  return useMemo(() => {
7
- if (path.length) {
8
- return Array.isArray(path) ? pathToString(path) : path
9
- }
10
- return documentRootKey
7
+ return getPathKey(path)
11
8
  }, [path])
12
9
  }
13
10
 
11
+ export function getPathKey(path: Path | string) {
12
+ if (path.length) {
13
+ return Array.isArray(path) ? pathToString(path) : path
14
+ }
15
+ return documentRootKey
16
+ }
17
+
14
18
  export function getInstructionTitle(instruction?: StudioInstruction) {
15
19
  return instruction?.title ?? 'Untitled'
16
20
  }
package/src/plugin.tsx CHANGED
@@ -16,7 +16,7 @@ import {isSchemaAssistEnabled} from './helpers/assistSupported'
16
16
  import {isImage} from './helpers/typeUtils'
17
17
  import {ImageContextProvider} from './components/ImageContext'
18
18
  import {TranslationConfig} from './translate/types'
19
- import {assistDocumentTypeName} from './types'
19
+ import {assistDocumentTypeName, AssistPreset} from './types'
20
20
 
21
21
  export interface AssistPluginConfig {
22
22
  translate?: TranslationConfig
@@ -25,6 +25,11 @@ export interface AssistPluginConfig {
25
25
  * @internal
26
26
  */
27
27
  __customApiClient?: (defaultClient: SanityClient) => SanityClient
28
+
29
+ /**
30
+ * @internal
31
+ */
32
+ __presets?: Record<string, AssistPreset>
28
33
  }
29
34
 
30
35
  export const assist = definePlugin<AssistPluginConfig | void>((config) => {
package/src/types.ts CHANGED
@@ -68,14 +68,42 @@ export interface AssistDocument extends SanityDocument {
68
68
  instructions?: StudioInstruction[]
69
69
  }
70
70
 
71
- export interface StudioAssistDocument {
71
+ export interface PresetInstruction {
72
+ _key: string
73
+ prompt?: PromptTextBlock[]
74
+
75
+ title?: string
76
+ /**
77
+ * String key from @sanity/icons IconMap
78
+ */
79
+ icon?: string
80
+
81
+ /**
82
+ * Type/field filter
83
+ */
84
+ output?: (OutputFieldItem | OutputTypeItem)[]
85
+ }
86
+
87
+ export interface PresetField {
88
+ path?: string
89
+ instructions?: PresetInstruction[]
90
+ }
91
+
92
+ export interface AssistPreset {
93
+ fields?: PresetField[]
94
+ }
95
+
96
+ export interface SanityAssistDocument {
72
97
  _id: string
73
98
  _type: typeof assistDocumentTypeName
74
99
  fields?: StudioAssistField[]
100
+ }
75
101
 
76
- // added
102
+ export interface StudioAssistDocument extends SanityAssistDocument {
103
+ // added after loading
77
104
  tasks?: InstructionTask[]
78
105
  }
106
+
79
107
  export interface AssistField {
80
108
  _key: string
81
109
  _type: typeof assistFieldTypeName
@@ -113,7 +141,10 @@ export interface UserInputBlock {
113
141
  }
114
142
 
115
143
  export type InlinePromptBlock = PortableTextSpan | FieldRef | UserInputBlock | ContextBlock
116
- export type PromptTextBlock = PortableTextBlock<never, InlinePromptBlock, 'normal', never>
144
+ export type PromptTextBlock = Omit<
145
+ PortableTextBlock<never, InlinePromptBlock, 'normal', never>,
146
+ '_type'
147
+ > & {_type: 'block'}
117
148
 
118
149
  export type PromptBlock = PromptTextBlock | FieldRef | ContextBlock | UserInputBlock
119
150
 
@@ -39,6 +39,10 @@ export interface TranslateRequest {
39
39
 
40
40
  const basePath = '/assist/tasks/instruction'
41
41
 
42
+ export function canUseAssist(status: InstructStatus | undefined) {
43
+ return status?.enabled && status.initialized && status.validToken
44
+ }
45
+
42
46
  export function useApiClient(customApiClient?: (defaultClient: SanityClient) => SanityClient) {
43
47
  const client = useClient({apiVersion: '2023-06-05'})
44
48
  return useMemo(