@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.
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.esm.js +2341 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +2341 -0
- package/dist/index.js.map +1 -0
- package/package.json +98 -0
- package/sanity.json +8 -0
- package/src/_lib/connector/ConnectFromRegion.tsx +24 -0
- package/src/_lib/connector/ConnectToRegion.tsx +22 -0
- package/src/_lib/connector/ConnectorRegion.tsx +23 -0
- package/src/_lib/connector/ConnectorsProvider.tsx +19 -0
- package/src/_lib/connector/ConnectorsStore.ts +122 -0
- package/src/_lib/connector/ConnectorsStoreContext.ts +4 -0
- package/src/_lib/connector/helpers.ts +5 -0
- package/src/_lib/connector/index.ts +9 -0
- package/src/_lib/connector/mapConnectorToLine.ts +83 -0
- package/src/_lib/connector/types.ts +56 -0
- package/src/_lib/connector/useConnectorsStore.ts +13 -0
- package/src/_lib/connector/useRegionRects.ts +141 -0
- package/src/_lib/fixedListenQuery.ts +101 -0
- package/src/_lib/form/DocumentForm.tsx +197 -0
- package/src/_lib/form/helpers.ts +31 -0
- package/src/_lib/form/index.ts +1 -0
- package/src/_lib/randomKey.ts +29 -0
- package/src/_lib/useListeningQuery.ts +61 -0
- package/src/_lib/usePrevious.ts +9 -0
- package/src/assistConnectors/AssistConnectorsOverlay.tsx +132 -0
- package/src/assistConnectors/ConnectorPath.tsx +62 -0
- package/src/assistConnectors/draw/arrowPath.ts +9 -0
- package/src/assistConnectors/draw/connectorPath.ts +142 -0
- package/src/assistConnectors/index.ts +1 -0
- package/src/assistDocument/AssistDocumentContext.tsx +31 -0
- package/src/assistDocument/AssistDocumentContextProvider.tsx +17 -0
- package/src/assistDocument/AssistDocumentInput.tsx +46 -0
- package/src/assistDocument/RequestRunInstructionProvider.tsx +50 -0
- package/src/assistDocument/components/AssistDocumentForm.tsx +188 -0
- package/src/assistDocument/components/FieldRefPreview.tsx +27 -0
- package/src/assistDocument/components/InstructionsArrayField.tsx +8 -0
- package/src/assistDocument/components/InstructionsArrayInput.tsx +26 -0
- package/src/assistDocument/components/SelectedFieldContext.tsx +10 -0
- package/src/assistDocument/components/generic/HiddenFieldTitle.tsx +5 -0
- package/src/assistDocument/components/helpers.ts +21 -0
- package/src/assistDocument/components/instruction/BackToInstructionsLink.tsx +31 -0
- package/src/assistDocument/components/instruction/FieldRefInput.tsx +33 -0
- package/src/assistDocument/components/instruction/InstructionInput.tsx +87 -0
- package/src/assistDocument/components/instruction/PromptInput.tsx +52 -0
- package/src/assistDocument/components/instruction/appearance/IconInput.tsx +46 -0
- package/src/assistDocument/components/instruction/appearance/InstructionVisibility.tsx +37 -0
- package/src/assistDocument/hooks/useAssistDocumentContextValue.tsx +68 -0
- package/src/assistDocument/hooks/useDocumentState.ts +6 -0
- package/src/assistDocument/hooks/useInstructionToaster.tsx +74 -0
- package/src/assistDocument/hooks/useStudioAssistDocument.ts +119 -0
- package/src/assistDocument/index.ts +1 -0
- package/src/assistFormComponents/AssistField.tsx +51 -0
- package/src/assistFormComponents/AssistFormBlock.tsx +31 -0
- package/src/assistFormComponents/AssistInlineFormBlock.tsx +14 -0
- package/src/assistFormComponents/AssistItem.tsx +20 -0
- package/src/assistFormComponents/validation/listItem.tsx +63 -0
- package/src/assistFormComponents/validation/validationList.tsx +89 -0
- package/src/assistInspector/AssistInspector.tsx +379 -0
- package/src/assistInspector/FieldAutocomplete.tsx +119 -0
- package/src/assistInspector/InstructionTaskHistoryButton.tsx +261 -0
- package/src/assistInspector/constants.ts +1 -0
- package/src/assistInspector/helpers.ts +125 -0
- package/src/assistInspector/index.ts +26 -0
- package/src/assistLayout/AiAssistanceConfigContext.tsx +81 -0
- package/src/assistLayout/AlphaMigration.tsx +311 -0
- package/src/assistLayout/AssistLayout.tsx +38 -0
- package/src/assistLayout/RunInstructionProvider.tsx +222 -0
- package/src/components/AssistFeatureBadge.tsx +9 -0
- package/src/components/Delay.tsx +25 -0
- package/src/components/HideReferenceChangedBannerInput.tsx +25 -0
- package/src/components/SafeValueInput.tsx +73 -0
- package/src/components/TimeAgo.tsx +18 -0
- package/src/constants.ts +20 -0
- package/src/fieldActions/PrivateIcon.tsx +20 -0
- package/src/fieldActions/assistFieldActions.tsx +230 -0
- package/src/globals.d.ts +4 -0
- package/src/helpers/assistSupported.ts +44 -0
- package/src/helpers/ids.ts +19 -0
- package/src/helpers/misc.ts +16 -0
- package/src/helpers/typeUtils.ts +15 -0
- package/src/helpers/useAssistSupported.ts +10 -0
- package/src/index.ts +6 -0
- package/src/legacy-types.ts +72 -0
- package/src/onboarding/FieldActionsOnboarding.tsx +90 -0
- package/src/onboarding/FirstAssistedPathProvider.tsx +29 -0
- package/src/onboarding/InspectorOnboarding.tsx +46 -0
- package/src/onboarding/onboardingStore.ts +33 -0
- package/src/plugin.tsx +80 -0
- package/src/presence/AiFieldPresence.tsx +28 -0
- package/src/presence/AssistAvatar.tsx +96 -0
- package/src/presence/AssistDocumentPresence.tsx +58 -0
- package/src/presence/useAssistPresence.ts +61 -0
- package/src/schemas/assistDocumentSchema.tsx +450 -0
- package/src/schemas/contextDocumentSchema.tsx +56 -0
- package/src/schemas/index.ts +25 -0
- package/src/schemas/serialize/SchemTypeTool.tsx +102 -0
- package/src/schemas/serialize/schemaUtils.ts +37 -0
- package/src/schemas/serialize/serializeSchema.test.ts +382 -0
- package/src/schemas/serialize/serializeSchema.ts +162 -0
- package/src/schemas/serializedSchemaTypeSchema.ts +59 -0
- package/src/schemas/typeDefExtensions.ts +30 -0
- package/src/types.ts +167 -0
- package/src/useApiClient.ts +140 -0
- package/src/vite.config.ts +9 -0
- 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
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
}
|
package/src/globals.d.ts
ADDED
|
@@ -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,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
|
+
}
|