@sanity/assist 1.0.0

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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +205 -0
  3. package/dist/index.d.ts +52 -0
  4. package/dist/index.esm.js +2341 -0
  5. package/dist/index.esm.js.map +1 -0
  6. package/dist/index.js +2341 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +98 -0
  9. package/sanity.json +8 -0
  10. package/src/_lib/connector/ConnectFromRegion.tsx +24 -0
  11. package/src/_lib/connector/ConnectToRegion.tsx +22 -0
  12. package/src/_lib/connector/ConnectorRegion.tsx +23 -0
  13. package/src/_lib/connector/ConnectorsProvider.tsx +19 -0
  14. package/src/_lib/connector/ConnectorsStore.ts +122 -0
  15. package/src/_lib/connector/ConnectorsStoreContext.ts +4 -0
  16. package/src/_lib/connector/helpers.ts +5 -0
  17. package/src/_lib/connector/index.ts +9 -0
  18. package/src/_lib/connector/mapConnectorToLine.ts +83 -0
  19. package/src/_lib/connector/types.ts +56 -0
  20. package/src/_lib/connector/useConnectorsStore.ts +13 -0
  21. package/src/_lib/connector/useRegionRects.ts +141 -0
  22. package/src/_lib/fixedListenQuery.ts +101 -0
  23. package/src/_lib/form/DocumentForm.tsx +197 -0
  24. package/src/_lib/form/helpers.ts +31 -0
  25. package/src/_lib/form/index.ts +1 -0
  26. package/src/_lib/randomKey.ts +29 -0
  27. package/src/_lib/useListeningQuery.ts +61 -0
  28. package/src/_lib/usePrevious.ts +9 -0
  29. package/src/assistConnectors/AssistConnectorsOverlay.tsx +132 -0
  30. package/src/assistConnectors/ConnectorPath.tsx +62 -0
  31. package/src/assistConnectors/draw/arrowPath.ts +9 -0
  32. package/src/assistConnectors/draw/connectorPath.ts +142 -0
  33. package/src/assistConnectors/index.ts +1 -0
  34. package/src/assistDocument/AssistDocumentContext.tsx +31 -0
  35. package/src/assistDocument/AssistDocumentContextProvider.tsx +17 -0
  36. package/src/assistDocument/AssistDocumentInput.tsx +46 -0
  37. package/src/assistDocument/RequestRunInstructionProvider.tsx +50 -0
  38. package/src/assistDocument/components/AssistDocumentForm.tsx +188 -0
  39. package/src/assistDocument/components/FieldRefPreview.tsx +27 -0
  40. package/src/assistDocument/components/InstructionsArrayField.tsx +8 -0
  41. package/src/assistDocument/components/InstructionsArrayInput.tsx +26 -0
  42. package/src/assistDocument/components/SelectedFieldContext.tsx +10 -0
  43. package/src/assistDocument/components/generic/HiddenFieldTitle.tsx +5 -0
  44. package/src/assistDocument/components/helpers.ts +21 -0
  45. package/src/assistDocument/components/instruction/BackToInstructionsLink.tsx +31 -0
  46. package/src/assistDocument/components/instruction/FieldRefInput.tsx +33 -0
  47. package/src/assistDocument/components/instruction/InstructionInput.tsx +87 -0
  48. package/src/assistDocument/components/instruction/PromptInput.tsx +52 -0
  49. package/src/assistDocument/components/instruction/appearance/IconInput.tsx +46 -0
  50. package/src/assistDocument/components/instruction/appearance/InstructionVisibility.tsx +37 -0
  51. package/src/assistDocument/hooks/useAssistDocumentContextValue.tsx +68 -0
  52. package/src/assistDocument/hooks/useDocumentState.ts +6 -0
  53. package/src/assistDocument/hooks/useInstructionToaster.tsx +74 -0
  54. package/src/assistDocument/hooks/useStudioAssistDocument.ts +119 -0
  55. package/src/assistDocument/index.ts +1 -0
  56. package/src/assistFormComponents/AssistField.tsx +51 -0
  57. package/src/assistFormComponents/AssistFormBlock.tsx +31 -0
  58. package/src/assistFormComponents/AssistInlineFormBlock.tsx +14 -0
  59. package/src/assistFormComponents/AssistItem.tsx +20 -0
  60. package/src/assistFormComponents/validation/listItem.tsx +63 -0
  61. package/src/assistFormComponents/validation/validationList.tsx +89 -0
  62. package/src/assistInspector/AssistInspector.tsx +379 -0
  63. package/src/assistInspector/FieldAutocomplete.tsx +119 -0
  64. package/src/assistInspector/InstructionTaskHistoryButton.tsx +261 -0
  65. package/src/assistInspector/constants.ts +1 -0
  66. package/src/assistInspector/helpers.ts +125 -0
  67. package/src/assistInspector/index.ts +26 -0
  68. package/src/assistLayout/AiAssistanceConfigContext.tsx +81 -0
  69. package/src/assistLayout/AlphaMigration.tsx +311 -0
  70. package/src/assistLayout/AssistLayout.tsx +38 -0
  71. package/src/assistLayout/RunInstructionProvider.tsx +222 -0
  72. package/src/components/AssistFeatureBadge.tsx +9 -0
  73. package/src/components/Delay.tsx +25 -0
  74. package/src/components/HideReferenceChangedBannerInput.tsx +25 -0
  75. package/src/components/SafeValueInput.tsx +73 -0
  76. package/src/components/TimeAgo.tsx +18 -0
  77. package/src/constants.ts +20 -0
  78. package/src/fieldActions/PrivateIcon.tsx +20 -0
  79. package/src/fieldActions/assistFieldActions.tsx +230 -0
  80. package/src/globals.d.ts +4 -0
  81. package/src/helpers/assistSupported.ts +44 -0
  82. package/src/helpers/ids.ts +19 -0
  83. package/src/helpers/misc.ts +16 -0
  84. package/src/helpers/typeUtils.ts +15 -0
  85. package/src/helpers/useAssistSupported.ts +10 -0
  86. package/src/index.ts +6 -0
  87. package/src/legacy-types.ts +72 -0
  88. package/src/onboarding/FieldActionsOnboarding.tsx +90 -0
  89. package/src/onboarding/FirstAssistedPathProvider.tsx +29 -0
  90. package/src/onboarding/InspectorOnboarding.tsx +46 -0
  91. package/src/onboarding/onboardingStore.ts +33 -0
  92. package/src/plugin.tsx +80 -0
  93. package/src/presence/AiFieldPresence.tsx +28 -0
  94. package/src/presence/AssistAvatar.tsx +96 -0
  95. package/src/presence/AssistDocumentPresence.tsx +58 -0
  96. package/src/presence/useAssistPresence.ts +61 -0
  97. package/src/schemas/assistDocumentSchema.tsx +450 -0
  98. package/src/schemas/contextDocumentSchema.tsx +56 -0
  99. package/src/schemas/index.ts +25 -0
  100. package/src/schemas/serialize/SchemTypeTool.tsx +102 -0
  101. package/src/schemas/serialize/schemaUtils.ts +37 -0
  102. package/src/schemas/serialize/serializeSchema.test.ts +382 -0
  103. package/src/schemas/serialize/serializeSchema.ts +162 -0
  104. package/src/schemas/serializedSchemaTypeSchema.ts +59 -0
  105. package/src/schemas/typeDefExtensions.ts +30 -0
  106. package/src/types.ts +167 -0
  107. package/src/useApiClient.ts +140 -0
  108. package/src/vite.config.ts +9 -0
  109. package/v2-incompatible.js +11 -0
@@ -0,0 +1,18 @@
1
+ import {useEffect, useReducer} from 'react'
2
+ import formatDistanceToNow from 'date-fns/formatDistanceToNow'
3
+
4
+ function useInterval(ms: number) {
5
+ const [tick, update] = useReducer((n) => n + 1, 0)
6
+
7
+ useEffect(() => {
8
+ const i = setInterval(update, ms)
9
+ return () => clearInterval(i)
10
+ }, [ms])
11
+ return tick
12
+ }
13
+
14
+ export function TimeAgo({date}: {date?: string}) {
15
+ useInterval(1000)
16
+ const timeSince = formatDistanceToNow(date ? new Date(date) : new Date())
17
+ return <span title={timeSince}>{timeSince} ago</span>
18
+ }
@@ -0,0 +1,20 @@
1
+ import {minutesToMilliseconds} from 'date-fns'
2
+
3
+ export const releaseAnnouncementUrl =
4
+ 'https://www.sanity.io/blog/sanity-ai-assist-announcement?utm_source=sanity-assist-plugin&utm_medium=organic_social&utm_campaign=ai-assist&utm_content='
5
+
6
+ export const instructionGuideUrl =
7
+ '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='
8
+
9
+ export const giveFeedbackUrl = 'https://forms.gle/Kwz7CThxGeA2GiEU8'
10
+
11
+ export const salesUrl =
12
+ 'https://www.sanity.io/contact/sales?utm_source=sanity-assist-plugin&utm_medium=organic_social&utm_campaign=ai-assist&utm_content='
13
+
14
+ export const packageName = '@sanity/assist'
15
+
16
+ export const pluginTitle = 'Sanity AI Assist'
17
+
18
+ export const pluginTitleShort = 'AI Assist'
19
+
20
+ export const maxHistoryVisibilityMs = minutesToMilliseconds(30)
@@ -0,0 +1,20 @@
1
+ import {LockIcon} from '@sanity/icons'
2
+ import {Text, Tooltip} from '@sanity/ui'
3
+
4
+ export function PrivateIcon() {
5
+ return (
6
+ <Tooltip
7
+ content={
8
+ <Text size={1} style={{whiteSpace: 'nowrap'}}>
9
+ Only visible to you
10
+ </Text>
11
+ }
12
+ fallbackPlacements={['bottom']}
13
+ padding={2}
14
+ placement="top"
15
+ portal
16
+ >
17
+ <LockIcon />
18
+ </Tooltip>
19
+ )
20
+ }
@@ -0,0 +1,230 @@
1
+ import {
2
+ DocumentFieldAction,
3
+ DocumentFieldActionGroup,
4
+ DocumentFieldActionItem,
5
+ ObjectSchemaType,
6
+ useCurrentUser,
7
+ } from 'sanity'
8
+ import {ControlsIcon, SparklesIcon} from '@sanity/icons'
9
+ import {useCallback, useMemo} from 'react'
10
+ import {pluginTitle, pluginTitleShort} from '../constants'
11
+ import {useAssistSupported} from '../helpers/useAssistSupported'
12
+ import {useAssistDocumentContext} from '../assistDocument/AssistDocumentContext'
13
+ import {getInstructionTitle, usePathKey} from '../helpers/misc'
14
+ import {fieldPathParam, instructionParam, StudioInstruction} from '../types'
15
+ import {aiInspectorId} from '../assistInspector/constants'
16
+ import {getIcon} from '../assistDocument/components/instruction/appearance/IconInput'
17
+ import {useAssistDocumentContextValue} from '../assistDocument/hooks/useAssistDocumentContextValue'
18
+ import {
19
+ getAssistableDocId,
20
+ useRequestRunInstruction,
21
+ } from '../assistDocument/RequestRunInstructionProvider'
22
+ import {PrivateIcon} from './PrivateIcon'
23
+
24
+ function node(node: DocumentFieldActionItem | DocumentFieldActionGroup) {
25
+ return node
26
+ }
27
+
28
+ export const assistFieldActions: DocumentFieldAction = {
29
+ name: 'sanity-assist-actions',
30
+ useAction(props) {
31
+ const assistSupported = useAssistSupported(props.path, props.schemaType)
32
+ const isDocumentLevel = props.path.length === 0
33
+
34
+ const {
35
+ assistDocument,
36
+ documentIsNew,
37
+ documentIsAssistable,
38
+ openInspector,
39
+ closeInspector,
40
+ inspector,
41
+ documentOnChange,
42
+ documentSchemaType,
43
+ documentId,
44
+ selectedPath,
45
+ } =
46
+ // document field actions do not have access to the document context
47
+ // conditional hook _should_ be safe here since the logical path will be stable
48
+ isDocumentLevel
49
+ ? // eslint-disable-next-line react-hooks/rules-of-hooks
50
+ useAssistDocumentContextValue(props.documentId, props.schemaType as ObjectSchemaType)
51
+ : // eslint-disable-next-line react-hooks/rules-of-hooks
52
+ useAssistDocumentContext()
53
+
54
+ const currentUser = useCurrentUser()
55
+ const isHidden = !assistDocument
56
+ const pathKey = usePathKey(props.path)
57
+ const assistDocumentId = assistDocument?._id
58
+
59
+ const assistableDocId = getAssistableDocId(documentSchemaType, documentId)
60
+ const {requestRunInstruction} = useRequestRunInstruction({
61
+ documentOnChange,
62
+ isDocAssistable: documentIsAssistable ?? false,
63
+ })
64
+
65
+ const fieldAssist = useMemo(
66
+ () => (assistDocument?.fields ?? []).find((f) => f.path == pathKey),
67
+ [assistDocument?.fields, pathKey]
68
+ )
69
+ const fieldAssistKey = fieldAssist?._key
70
+ const isInspectorOpen = inspector?.name === aiInspectorId
71
+ const isPathSelected = pathKey === selectedPath
72
+ const isSelected = isInspectorOpen && isPathSelected
73
+
74
+ const manageInstructions = useCallback(
75
+ () =>
76
+ isSelected
77
+ ? closeInspector(aiInspectorId)
78
+ : openInspector(aiInspectorId, {
79
+ [fieldPathParam]: pathKey,
80
+ [instructionParam]: undefined as any,
81
+ }),
82
+ [openInspector, closeInspector, isSelected, pathKey]
83
+ )
84
+
85
+ const onInstructionAction = useCallback(
86
+ (instruction: StudioInstruction) => {
87
+ if (!pathKey || !fieldAssistKey || !assistDocumentId || !assistableDocId) {
88
+ return
89
+ }
90
+ requestRunInstruction({
91
+ documentId: assistableDocId,
92
+ assistDocumentId,
93
+ path: pathKey,
94
+ instruction,
95
+ })
96
+ },
97
+ [requestRunInstruction, assistableDocId, pathKey, assistDocumentId, fieldAssistKey]
98
+ )
99
+
100
+ const privateInstructions = useMemo(
101
+ () =>
102
+ fieldAssist?.instructions?.filter((i) => i.userId && i.userId === currentUser?.id) || [],
103
+ [fieldAssist?.instructions, currentUser]
104
+ )
105
+
106
+ const sharedInstructions = useMemo(
107
+ () => fieldAssist?.instructions?.filter((i) => !i.userId) || [],
108
+ [fieldAssist?.instructions]
109
+ )
110
+
111
+ const instructions = useMemo(
112
+ () => [...privateInstructions, ...sharedInstructions],
113
+ [privateInstructions, sharedInstructions]
114
+ )
115
+
116
+ const runInstructionsGroup = useMemo(() => {
117
+ return instructions?.length
118
+ ? node({
119
+ type: 'group',
120
+ icon: () => null,
121
+ title: 'Run instructions',
122
+ children: instructions.map((instruction) =>
123
+ instructionItem({
124
+ instruction,
125
+ isPrivate: Boolean(instruction.userId && instruction.userId === currentUser?.id),
126
+ onInstructionAction,
127
+ hidden: isHidden,
128
+ documentIsNew: !!documentIsNew,
129
+ assistSupported,
130
+ })
131
+ ),
132
+ expanded: true,
133
+ })
134
+ : undefined
135
+ }, [
136
+ instructions,
137
+ currentUser?.id,
138
+ onInstructionAction,
139
+ isHidden,
140
+ documentIsNew,
141
+ assistSupported,
142
+ ])
143
+
144
+ const instructionsLength = instructions?.length || 0
145
+
146
+ const manageInstructionsItem = useMemo(
147
+ () =>
148
+ node({
149
+ type: 'action',
150
+ icon: ControlsIcon,
151
+ title: 'Manage instructions',
152
+ onAction: manageInstructions,
153
+ selected: isSelected,
154
+ }),
155
+ [manageInstructions, isSelected]
156
+ )
157
+
158
+ const group = useMemo(
159
+ () =>
160
+ node({
161
+ type: 'group',
162
+ icon: SparklesIcon,
163
+ title: pluginTitleShort,
164
+ children: [
165
+ runInstructionsGroup,
166
+ /* documentIsNew && {
167
+ type: 'action',
168
+ disabled: true,
169
+ title: `Document is new. Make an edit to enable `,
170
+ icon: WarningOutlineIcon,
171
+ tone: 'caution',
172
+ status: 'warning',
173
+ onAction: () => {},
174
+ },*/
175
+ manageInstructionsItem,
176
+ ].filter((c): c is DocumentFieldActionItem | DocumentFieldActionGroup => !!c),
177
+ expanded: false,
178
+ renderAsButton: true,
179
+ hidden: !assistSupported,
180
+ }),
181
+ [
182
+ //documentIsNew,
183
+ runInstructionsGroup,
184
+ manageInstructionsItem,
185
+ assistSupported,
186
+ ]
187
+ )
188
+
189
+ const emptyAction = useMemo(
190
+ () =>
191
+ node({
192
+ type: 'action',
193
+ hidden: !assistSupported,
194
+ icon: SparklesIcon,
195
+ onAction: manageInstructions,
196
+ renderAsButton: true,
197
+ title: pluginTitleShort,
198
+ selected: isSelected,
199
+ }),
200
+ [assistSupported, manageInstructions, isSelected]
201
+ )
202
+
203
+ // If there are no instructions, we don't want to render the group
204
+ if (instructionsLength === 0) {
205
+ return emptyAction
206
+ }
207
+
208
+ return group
209
+ },
210
+ }
211
+
212
+ function instructionItem(props: {
213
+ instruction: StudioInstruction
214
+ isPrivate: boolean
215
+ onInstructionAction: (ins: StudioInstruction) => void
216
+ assistSupported: boolean
217
+ documentIsNew: boolean
218
+ hidden: boolean
219
+ }) {
220
+ const {hidden, isPrivate, onInstructionAction, assistSupported, instruction} = props
221
+ return node({
222
+ type: 'action',
223
+ icon: getIcon(instruction.icon),
224
+ iconRight: isPrivate ? PrivateIcon : undefined,
225
+ title: getInstructionTitle(instruction),
226
+ onAction: () => onInstructionAction(instruction),
227
+ disabled: assistSupported ? false : {reason: `${pluginTitle} is not supported for this field`},
228
+ hidden,
229
+ })
230
+ }
@@ -0,0 +1,4 @@
1
+ declare module '*.png'
2
+ declare module '*.svg'
3
+ declare module '*.jpeg'
4
+ declare module '*.jpg'
@@ -0,0 +1,44 @@
1
+ import {SchemaType} from 'sanity'
2
+ import {AssistOptions} from '../schemas/typeDefExtensions'
3
+ import {isType} from './typeUtils'
4
+
5
+ export function isSchemaAssistEnabled(type: SchemaType) {
6
+ return !(type.options as AssistOptions | undefined)?.aiWritingAssistance?.exclude
7
+ }
8
+
9
+ export function isAssistSupported(type: SchemaType) {
10
+ if (!isSchemaAssistEnabled(type)) {
11
+ return false
12
+ }
13
+
14
+ if (isUnsupportedType(type)) {
15
+ return false
16
+ }
17
+
18
+ if (type.jsonType === 'array') {
19
+ const unsupportedArray = type.of.every((t) => isUnsupportedType(t))
20
+ return !unsupportedArray
21
+ }
22
+
23
+ if (type.jsonType === 'object') {
24
+ const unsupportedObject = type.fields.every((field) => isUnsupportedType(field.type))
25
+ return !unsupportedObject
26
+ }
27
+ return true
28
+ }
29
+
30
+ function isUnsupportedType(type: SchemaType) {
31
+ return (
32
+ !isSchemaAssistEnabled(type) ||
33
+ type.jsonType === 'number' ||
34
+ type.name === 'sanity.imageCrop' ||
35
+ type.name === 'sanity.imageHotspot' ||
36
+ isType(type, 'reference') ||
37
+ isType(type, 'crossDatasetReference') ||
38
+ isType(type, 'slug') ||
39
+ isType(type, 'url') ||
40
+ isType(type, 'date') ||
41
+ isType(type, 'datetime') ||
42
+ isType(type, 'file')
43
+ )
44
+ }
@@ -0,0 +1,19 @@
1
+ import {assistDocumentIdPrefix, assistDocumentStatusIdPrefix} from '../types'
2
+
3
+ const aiDocPrefixPattern = new RegExp(`^${assistDocumentIdPrefix}`)
4
+
5
+ export function publicId(id: string) {
6
+ return id.replace('drafts.', '')
7
+ }
8
+
9
+ export function assistDocumentId(documentType: string) {
10
+ return `${assistDocumentIdPrefix}${documentType}`
11
+ }
12
+
13
+ export function documentTypeFromAiDocumentId(id: string) {
14
+ return id.replace(aiDocPrefixPattern, '')
15
+ }
16
+
17
+ export function assistTasksStatusId(documentId: string) {
18
+ return `${assistDocumentStatusIdPrefix}${publicId(documentId)}`
19
+ }
@@ -0,0 +1,16 @@
1
+ import {documentRootKey, StudioInstruction} from '../types'
2
+ import {Path, pathToString} from 'sanity'
3
+ import {useMemo} from 'react'
4
+
5
+ export function usePathKey(path: Path | string) {
6
+ return useMemo(() => {
7
+ if (path.length) {
8
+ return Array.isArray(path) ? pathToString(path) : path
9
+ }
10
+ return documentRootKey
11
+ }, [path])
12
+ }
13
+
14
+ export function getInstructionTitle(instruction?: StudioInstruction) {
15
+ return instruction?.title ?? 'Untitled'
16
+ }
@@ -0,0 +1,15 @@
1
+ import {ArraySchemaType, SchemaType} from 'sanity'
2
+
3
+ export function isPortableTextArray(type: ArraySchemaType) {
4
+ return type.of.find((t) => isType(t, 'block'))
5
+ }
6
+
7
+ export function isType(schemaType: SchemaType, typeName: string): boolean {
8
+ if (schemaType.name === typeName) {
9
+ return true
10
+ }
11
+ if (!schemaType.type) {
12
+ return false
13
+ }
14
+ return isType(schemaType.type, typeName)
15
+ }
@@ -0,0 +1,10 @@
1
+ import {Path, SchemaType} from 'sanity'
2
+ import {useMemo} from 'react'
3
+ import {isAssistSupported} from './assistSupported'
4
+
5
+ export function useAssistSupported(path: Path, schemaType: SchemaType) {
6
+ return useMemo(
7
+ () => path.every((p) => typeof p === 'string') && isAssistSupported(schemaType),
8
+ [path, schemaType]
9
+ )
10
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './schemas/typeDefExtensions'
2
+ export * from './schemas/serialize/SchemTypeTool'
3
+
4
+ export {contextDocumentTypeName} from './types'
5
+
6
+ export {assist} from './plugin'
@@ -0,0 +1,72 @@
1
+ import {SanityDocument} from 'sanity'
2
+ import {PortableTextBlock, PortableTextMarkDefinition, PortableTextSpan} from '@portabletext/types'
3
+
4
+ //id prefixes
5
+ export const legacyAssistDocumentIdPrefix = 'sanity.ai.'
6
+
7
+ export const legacyAssistDocumentTypeName = 'sanity.ai.docType' as const
8
+ const aiFieldTypeName = 'sanity.ai.docType.field' as const
9
+ const instructionTypeName = 'sanity.ai.field.instruction' as const
10
+
11
+ const userInputTypeName = 'sanity.ai.prompt.userInput' as const
12
+ const promptContextTypeName = 'sanity.ai.prompt.context' as const
13
+ const fieldReferenceTypeName = 'sanity.ai.prompt.fieldRef' as const
14
+
15
+ export const legacyContextDocumentTypeName = 'ai.instruction.context' as const
16
+
17
+ export const legacyAssistStatusDocumentTypeName = 'sanity.ai.instructionStatus' as const
18
+
19
+ export interface FieldPrompts {
20
+ _key: string
21
+ _type: typeof aiFieldTypeName
22
+ path?: string
23
+ instructions?: LegacyInstruction[]
24
+ }
25
+
26
+ export interface LegacyAssistDocument extends SanityDocument {
27
+ fields: FieldPrompts[]
28
+ }
29
+
30
+ export interface LegacyFieldRef extends PortableTextMarkDefinition {
31
+ _type: typeof fieldReferenceTypeName
32
+ path?: string
33
+ }
34
+
35
+ export interface LegacyContextBlock {
36
+ _type: typeof promptContextTypeName
37
+ reference?: {
38
+ _type: 'reference'
39
+ _ref?: string
40
+ }
41
+ }
42
+
43
+ export interface LegacyUserInputBlock {
44
+ _type: typeof userInputTypeName
45
+ _key: string
46
+ message?: string
47
+ description?: string
48
+ }
49
+
50
+ export type LegacyPromptTextBlock = PortableTextBlock<
51
+ never,
52
+ PortableTextSpan | LegacyFieldRef | LegacyUserInputBlock | LegacyContextBlock,
53
+ 'normal',
54
+ never
55
+ >
56
+
57
+ export type LegacyPromptBlock =
58
+ | LegacyPromptTextBlock
59
+ | LegacyFieldRef
60
+ | LegacyContextBlock
61
+ | LegacyUserInputBlock
62
+
63
+ export interface LegacyInstruction {
64
+ _key: string
65
+ _type: typeof instructionTypeName
66
+ prompt?: LegacyPromptBlock[]
67
+
68
+ icon?: string
69
+ userId?: string
70
+ title?: string
71
+ placeholder?: string
72
+ }
@@ -0,0 +1,90 @@
1
+ import {Path, pathToString} from 'sanity'
2
+ import {Button, Flex, Popover, Stack, Text} from '@sanity/ui'
3
+ import {PropsWithChildren, useContext, useMemo, useRef} from 'react'
4
+ import {FirstAssistedPathContext} from './FirstAssistedPathProvider'
5
+ import {AssistFeatureBadge} from '../components/AssistFeatureBadge'
6
+ import {ArrowRightIcon, CheckmarkIcon, SparklesIcon} from '@sanity/icons'
7
+ import {pluginTitle, releaseAnnouncementUrl} from '../constants'
8
+ import {fieldOnboardingKey, useOnboardingFeature} from './onboardingStore'
9
+
10
+ export interface FieldActionsOnboardingProps {
11
+ path: Path
12
+ }
13
+
14
+ export function FieldActionsOnboarding(props: PropsWithChildren<FieldActionsOnboardingProps>) {
15
+ const {path, children} = props
16
+
17
+ const firstAssistedPath = useContext(FirstAssistedPathContext)
18
+ const isFirstAssisted = useMemo(
19
+ () => pathToString(path) === firstAssistedPath,
20
+ [path, firstAssistedPath]
21
+ )
22
+
23
+ if (!isFirstAssisted) {
24
+ return <>{children}</>
25
+ }
26
+
27
+ return <AssistOnboardingPopover {...props} />
28
+ }
29
+
30
+ function AssistOnboardingPopover(props: PropsWithChildren<FieldActionsOnboardingProps>) {
31
+ const {showOnboarding, dismissOnboarding} = useOnboardingFeature(fieldOnboardingKey)
32
+
33
+ if (!showOnboarding) {
34
+ return <>{props.children}</>
35
+ }
36
+
37
+ return (
38
+ <Popover
39
+ content={<AssistIntroCard path={props.path} dismiss={dismissOnboarding} />}
40
+ open
41
+ portal
42
+ placeholder="bottom"
43
+ tone="default"
44
+ width={0}
45
+ >
46
+ <div>
47
+ <Button disabled fontSize={1} icon={SparklesIcon} mode="ghost" padding={2} />
48
+ </div>
49
+ </Popover>
50
+ )
51
+ }
52
+
53
+ function AssistIntroCard(props: FieldActionsOnboardingProps & {dismiss: () => void}) {
54
+ const buttonRef = useRef<HTMLButtonElement>(null)
55
+
56
+ return (
57
+ <Stack as="section" padding={3} space={3}>
58
+ <Stack padding={2} space={4}>
59
+ <Flex gap={2} align="center">
60
+ <Text as="h1" size={1} weight="semibold">
61
+ {pluginTitle}
62
+ </Text>
63
+ <div aria-hidden style={{margin: '-3px 0', lineHeight: 0}}>
64
+ <AssistFeatureBadge />
65
+ </div>
66
+ </Flex>
67
+
68
+ <Stack space={3}>
69
+ <Text as="p" muted size={1}>
70
+ Manage reusable AI instructions to boost your content creation and reduce amount of
71
+ repetitive chores.{' '}
72
+ <a href={releaseAnnouncementUrl} target="_blank" rel="noreferrer">
73
+ Learn more <ArrowRightIcon />
74
+ </a>
75
+ </Text>
76
+ </Stack>
77
+ </Stack>
78
+
79
+ <Button
80
+ fontSize={1}
81
+ icon={CheckmarkIcon}
82
+ onClick={props.dismiss}
83
+ padding={3}
84
+ ref={buttonRef}
85
+ text="Ok, good to know!"
86
+ tone="primary"
87
+ />
88
+ </Stack>
89
+ )
90
+ }
@@ -0,0 +1,29 @@
1
+ import {ObjectInputProps, FieldMember, pathToString} from 'sanity'
2
+ import {createContext, PropsWithChildren, useMemo} from 'react'
3
+ import {isAssistSupported} from '../helpers/assistSupported'
4
+
5
+ export const FirstAssistedPathContext = createContext<string | undefined>(undefined)
6
+
7
+ export interface FirstAssistedPathProviderProps {
8
+ members: ObjectInputProps['members']
9
+ }
10
+
11
+ export function FirstAssistedPathProvider(
12
+ props: PropsWithChildren<FirstAssistedPathProviderProps>
13
+ ) {
14
+ const {members} = props
15
+
16
+ const firstAssistedPath = useMemo(() => {
17
+ const firstAssisted = members.find(
18
+ (member): member is FieldMember =>
19
+ member.kind === 'field' && isAssistSupported(member.field.schemaType)
20
+ )
21
+ return firstAssisted?.field.path ? pathToString(firstAssisted?.field.path) : undefined
22
+ }, [members])
23
+
24
+ return (
25
+ <FirstAssistedPathContext.Provider value={firstAssistedPath}>
26
+ {props.children}
27
+ </FirstAssistedPathContext.Provider>
28
+ )
29
+ }
@@ -0,0 +1,46 @@
1
+ import {Box, Button, Container, Flex, Stack, Text} from '@sanity/ui'
2
+ import {SparklesIcon} from '@sanity/icons'
3
+ import {releaseAnnouncementUrl} from '../constants'
4
+ import styled from 'styled-components'
5
+
6
+ const SparklesIllustration = styled(SparklesIcon)({
7
+ fontSize: '3.125em',
8
+ '& path': {
9
+ strokeWidth: `0.6px !important`,
10
+ },
11
+ })
12
+
13
+ export function InspectorOnboarding(props: {onDismiss: () => void}) {
14
+ const {onDismiss} = props
15
+ return (
16
+ <Box padding={4}>
17
+ <Container width={0}>
18
+ <Stack space={4}>
19
+ <div style={{textAlign: 'center'}}>
20
+ <SparklesIllustration />
21
+ </div>
22
+ <Text align="center" size={1}>
23
+ Create reusable AI instructions that can be applied across all documents of a certain
24
+ type.
25
+ </Text>
26
+
27
+ <Flex align="center" gap={2} justify="center">
28
+ <Button
29
+ as="a"
30
+ href={releaseAnnouncementUrl}
31
+ rel="noreferrer"
32
+ target="_blank"
33
+ fontSize={1}
34
+ mode="default"
35
+ onClick={onDismiss}
36
+ padding={2}
37
+ text="Learn more"
38
+ tone="primary"
39
+ />
40
+ <Button fontSize={1} mode="bleed" onClick={onDismiss} padding={2} text="Dismiss" />
41
+ </Flex>
42
+ </Stack>
43
+ </Container>
44
+ </Box>
45
+ )
46
+ }
@@ -0,0 +1,33 @@
1
+ import {useCallback, useState} from 'react'
2
+
3
+ export const inspectorOnboardingKey = 'sanityStudio:assist:inspector:onboarding:dismissed'
4
+ export const fieldOnboardingKey = 'sanityStudio:assist:field:onboarding:dismissed'
5
+
6
+ export function isFeatureOnboardingDismissed(featureKey: string): boolean {
7
+ if (typeof localStorage === 'undefined') {
8
+ return false
9
+ }
10
+
11
+ const value = localStorage.getItem(featureKey)
12
+ return value === 'true'
13
+ }
14
+
15
+ export function dismissFeatureOnboarding(featureKey: string) {
16
+ if (typeof localStorage === 'undefined') {
17
+ return
18
+ }
19
+
20
+ localStorage.setItem(featureKey, 'true')
21
+ }
22
+
23
+ export function useOnboardingFeature(featureKey: string) {
24
+ const [showOnboarding, setShowOnboarding] = useState(
25
+ () => !isFeatureOnboardingDismissed(featureKey)
26
+ )
27
+ const dismissOnboarding = useCallback(() => {
28
+ setShowOnboarding(false)
29
+ dismissFeatureOnboarding(featureKey)
30
+ }, [setShowOnboarding, featureKey])
31
+
32
+ return {showOnboarding, dismissOnboarding}
33
+ }