@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,311 @@
1
+ import {SanityClient, SanityDocument, useClient} from 'sanity'
2
+ import {useCallback, useEffect, useState} from 'react'
3
+ import {Box, Button, Card, Dialog, Spinner, Stack, Text, useToast} from '@sanity/ui'
4
+ import {CheckmarkIcon} from '@sanity/icons'
5
+ import {
6
+ LegacyAssistDocument,
7
+ legacyAssistDocumentIdPrefix,
8
+ legacyAssistDocumentTypeName,
9
+ legacyAssistStatusDocumentTypeName,
10
+ LegacyContextBlock,
11
+ legacyContextDocumentTypeName,
12
+ LegacyFieldRef,
13
+ LegacyPromptBlock,
14
+ LegacyPromptTextBlock,
15
+ LegacyUserInputBlock,
16
+ } from '../legacy-types'
17
+ import {assistDocumentId} from '../helpers/ids'
18
+ import {
19
+ assistDocumentIdPrefix,
20
+ assistDocumentTypeName,
21
+ AssistField,
22
+ assistFieldTypeName,
23
+ contextDocumentTypeName,
24
+ fieldReferenceTypeName,
25
+ InlinePromptBlock,
26
+ instructionContextTypeName,
27
+ instructionTypeName,
28
+ PromptBlock,
29
+ userInputTypeName,
30
+ } from '../types'
31
+ import {PortableTextSpan} from '@portabletext/types'
32
+ import {pluginTitle} from '../constants'
33
+
34
+ const NO_ASSIST_DOCS: LegacyAssistDocument[] = []
35
+ const NO_CONTEXT_DOCS: SanityDocument[] = []
36
+ const NO_IDS: string[] = []
37
+
38
+ interface MigratedContextDoc {
39
+ _id: string
40
+ _alphaId: string
41
+ }
42
+
43
+ type Task = (subtaskProgress: (percentage: number) => void) => Promise<void>
44
+
45
+ export function AlphaMigration() {
46
+ const [alphaAssistDocs, setAlphaAssistDocs] = useState(NO_ASSIST_DOCS)
47
+ const [contextDocs, setContextDocs] = useState(NO_CONTEXT_DOCS)
48
+ const [staleStatusDocIds, setStaleStatusDocs] = useState(NO_IDS)
49
+ const [error, setError] = useState<Error | undefined>(undefined)
50
+ const [progress, setProgress] = useState<number | undefined>(undefined)
51
+ const toast = useToast()
52
+ const client = useClient({apiVersion: '2023-06-01'})
53
+
54
+ useEffect(() => {
55
+ let canUpdate = true
56
+ client
57
+ .fetch<{
58
+ assistDocs?: LegacyAssistDocument[]
59
+ staleStatusDocIds?: string[]
60
+ contextDocs?: SanityDocument[]
61
+ }>(
62
+ `
63
+ {
64
+ "assistDocs": *[_type=="${legacyAssistDocumentTypeName}"],
65
+ "staleStatusDocIds": *[_type=="${legacyAssistStatusDocumentTypeName}"]._id,
66
+ "contextDocs": *[_type=="${legacyContextDocumentTypeName}"],
67
+ }
68
+ `
69
+ )
70
+ .then((result) => {
71
+ if (!canUpdate || !result) {
72
+ return
73
+ }
74
+ setAlphaAssistDocs(result?.assistDocs ?? NO_ASSIST_DOCS)
75
+ setStaleStatusDocs(result?.staleStatusDocIds ?? NO_IDS)
76
+ setContextDocs(result?.contextDocs ?? NO_CONTEXT_DOCS)
77
+ })
78
+ return () => {
79
+ canUpdate = false
80
+ }
81
+ }, [client, setAlphaAssistDocs, setStaleStatusDocs, setContextDocs])
82
+
83
+ const convert = useCallback(async () => {
84
+ try {
85
+ setProgress(0.0001)
86
+
87
+ const tasks: Task[] = [
88
+ () => convertContextDocs(client, contextDocs),
89
+ (subtaskProgress) => deleteDocs(client, staleStatusDocIds, subtaskProgress),
90
+ (subtaskProgress) => convertDocs(client, alphaAssistDocs, subtaskProgress),
91
+ (subtaskProgress) =>
92
+ deleteDocs(
93
+ client,
94
+ contextDocs.map((d) => d._id),
95
+ subtaskProgress
96
+ ),
97
+ ]
98
+
99
+ const taskSize = 1 / tasks.length
100
+ for (let i = 0; i < tasks.length; i++) {
101
+ const startProgress = i / tasks.length
102
+ await tasks[i]((subProgress) => setProgress(startProgress + subProgress * taskSize))
103
+ setProgress((i + 1) / tasks.length)
104
+ }
105
+
106
+ setProgress(1)
107
+ setAlphaAssistDocs(NO_ASSIST_DOCS)
108
+ setContextDocs(NO_CONTEXT_DOCS)
109
+ setStaleStatusDocs(NO_IDS)
110
+ toast.push({
111
+ title: `Converted instructions to new format.`,
112
+ status: 'success',
113
+ description: 'Pending feature name',
114
+ closable: true,
115
+ })
116
+ } catch (e: any) {
117
+ console.error(e)
118
+ toast.push({
119
+ title: `An error occured`,
120
+ status: 'error',
121
+ closable: true,
122
+ })
123
+ setError(e)
124
+ setProgress(undefined)
125
+ }
126
+ }, [contextDocs, client, alphaAssistDocs, staleStatusDocIds, setProgress, toast])
127
+
128
+ if (
129
+ (alphaAssistDocs.length || staleStatusDocIds.length || contextDocs.length) &&
130
+ (!progress || progress < 1)
131
+ ) {
132
+ return (
133
+ <Dialog id="outdated-assist-docs" header={pluginTitle}>
134
+ <Card padding={3}>
135
+ <Stack space={4} style={{maxWidth: 500}}>
136
+ <Text size={1}>
137
+ It seems like this workspace contains documents from an{' '}
138
+ <strong>older version of {pluginTitle}</strong>.
139
+ </Text>
140
+ <Text size={1}>Cleanup is required.</Text>
141
+ {error ? (
142
+ <Card padding={2} tone="critical" border>
143
+ <Text size={1}>An error occurred. See console for details.</Text>{' '}
144
+ </Card>
145
+ ) : null}
146
+ <Button
147
+ icon={
148
+ progress ? (
149
+ <Box style={{marginTop: 5}}>
150
+ <Spinner />
151
+ </Box>
152
+ ) : (
153
+ CheckmarkIcon
154
+ )
155
+ }
156
+ disabled={!!progress}
157
+ text={progress ? `${Math.floor(progress * 100)}%` : 'Ok, convert to new format!'}
158
+ tone="primary"
159
+ onClick={convert}
160
+ />
161
+ </Stack>
162
+ </Card>
163
+ </Dialog>
164
+ )
165
+ }
166
+
167
+ return null
168
+ }
169
+
170
+ async function deleteDocs(
171
+ client: SanityClient,
172
+ ids: string[],
173
+ updateProgress: (percentage: number) => void
174
+ ) {
175
+ const chunkSize = 50
176
+ for (let i = 0; i < ids.length; i += chunkSize) {
177
+ const progressCount = Math.min(ids.length, i + chunkSize)
178
+ const chunk = ids.slice(i, progressCount)
179
+ const trans = client.transaction()
180
+ chunk.forEach((id) => trans.delete(id))
181
+ await trans.commit()
182
+ updateProgress(progressCount / ids.length)
183
+ }
184
+ }
185
+
186
+ async function convertContextDocs(client: SanityClient, docs: SanityDocument[]) {
187
+ const trans = client.transaction()
188
+
189
+ for (const doc of docs) {
190
+ const {_id, _type, ...rest} = doc
191
+ trans.createOrReplace({
192
+ ...rest,
193
+ _id: `port.${_id}`,
194
+ _alphaId: _id,
195
+ _type: contextDocumentTypeName,
196
+ })
197
+ }
198
+
199
+ await trans.commit()
200
+ }
201
+
202
+ async function convertDocs(
203
+ client: SanityClient,
204
+ docs: LegacyAssistDocument[],
205
+ updateProgress: (percentage: number) => void
206
+ ) {
207
+ const chunkSize = 10
208
+ for (let i = 0; i < docs.length; i += chunkSize) {
209
+ const progressCount = Math.min(docs.length, i + chunkSize)
210
+ const chunk = docs.slice(i, progressCount)
211
+
212
+ const trans = client.transaction()
213
+ const contextDocs: MigratedContextDoc[] = await client.fetch(
214
+ `*[_type=="${contextDocumentTypeName}" && _alphaId != null]{_id, _alphaId}`
215
+ )
216
+
217
+ chunk.forEach((oldDoc) => {
218
+ const documentType = oldDoc._id.replace(
219
+ new RegExp(`^(${legacyAssistDocumentIdPrefix}|${assistDocumentIdPrefix})`),
220
+ ''
221
+ )
222
+
223
+ const id = assistDocumentId(documentType)
224
+
225
+ const fields: AssistField[] = (oldDoc.fields ?? [])
226
+ .filter((field) => field.instructions?.length)
227
+ .map((oldField) => {
228
+ const instructions = (oldField.instructions ?? []).map((inst) => {
229
+ // eslint-disable-next-line max-nested-callbacks
230
+ const prompt = (inst.prompt ?? []).map((block) => {
231
+ return mapBlock(block, contextDocs) as PromptBlock
232
+ })
233
+ return {
234
+ ...inst,
235
+ _type: instructionTypeName,
236
+ prompt,
237
+ }
238
+ })
239
+ return {
240
+ ...oldField,
241
+ _type: assistFieldTypeName,
242
+ instructions,
243
+ }
244
+ })
245
+
246
+ if (fields.length) {
247
+ trans.createOrReplace({
248
+ _id: id,
249
+ _type: assistDocumentTypeName,
250
+ fields,
251
+ })
252
+ }
253
+ trans.delete(oldDoc._id)
254
+ })
255
+ await trans.commit()
256
+ updateProgress(progressCount / docs.length)
257
+ }
258
+ }
259
+
260
+ type Blocks = LegacyPromptBlock | LegacyPromptTextBlock | PortableTextSpan
261
+
262
+ function isFieldRef(block: Blocks): block is LegacyFieldRef {
263
+ return block._type === 'sanity.ai.prompt.fieldRef'
264
+ }
265
+
266
+ function isContext(block: Blocks): block is LegacyContextBlock {
267
+ return block._type === 'sanity.ai.prompt.context'
268
+ }
269
+
270
+ function isUserInput(block: Blocks): block is LegacyUserInputBlock {
271
+ return block._type === 'sanity.ai.prompt.userInput'
272
+ }
273
+
274
+ function isSpan(block: Blocks): block is PortableTextSpan {
275
+ return block._type === 'span'
276
+ }
277
+
278
+ function mapBlock(
279
+ block: Blocks,
280
+ migratedContexts: MigratedContextDoc[]
281
+ ): PromptBlock | InlinePromptBlock {
282
+ if (isFieldRef(block)) {
283
+ return {...block, _type: fieldReferenceTypeName}
284
+ }
285
+ if (isUserInput(block)) {
286
+ return {...block, _type: userInputTypeName}
287
+ }
288
+ if (isContext(block)) {
289
+ const newBlock = {
290
+ ...block,
291
+ _type: instructionContextTypeName,
292
+ reference: {
293
+ _type: 'reference',
294
+ _ref:
295
+ migratedContexts.find((c) => c._alphaId === block.reference?._ref)?._id ??
296
+ block.reference?._ref,
297
+ },
298
+ } as const
299
+ return newBlock
300
+ }
301
+ if (isSpan(block)) {
302
+ return block
303
+ }
304
+ const textBlock = block
305
+ return {
306
+ ...textBlock,
307
+ children: (textBlock.children ?? []).map(
308
+ (child) => mapBlock(child, migratedContexts) as InlinePromptBlock
309
+ ),
310
+ }
311
+ }
@@ -0,0 +1,38 @@
1
+ import {useState} from 'react'
2
+ import {LayoutProps} from 'sanity'
3
+ import {Connector, ConnectorsProvider} from '../_lib/connector'
4
+ import {AssistConnectorsOverlay} from '../assistConnectors'
5
+ import {AssistPluginConfig} from '../plugin'
6
+ import {AiAssistanceConfigProvider} from './AiAssistanceConfigContext'
7
+ import {RunInstructionRequest} from '../useApiClient'
8
+ import {StudioInstruction} from '../types'
9
+ import {RunInstructionProvider} from './RunInstructionProvider'
10
+ import {ThemeProvider} from '@sanity/ui'
11
+ import {AlphaMigration} from './AlphaMigration'
12
+
13
+ export interface AIStudioLayoutProps extends LayoutProps {
14
+ config: AssistPluginConfig
15
+ }
16
+
17
+ export type RunInstructionArgs = Omit<RunInstructionRequest, 'instructionKey' | 'userText'> & {
18
+ instruction: StudioInstruction
19
+ }
20
+
21
+ export function AssistLayout(props: AIStudioLayoutProps) {
22
+ const [connectors, setConnectors] = useState<Connector[]>([])
23
+ const migrate = props.config.alphaMigration ?? true
24
+
25
+ return (
26
+ <AiAssistanceConfigProvider config={props.config}>
27
+ {migrate ? <AlphaMigration /> : null}
28
+ <RunInstructionProvider>
29
+ <ConnectorsProvider onConnectorsChange={setConnectors}>
30
+ {props.renderDefault(props)}
31
+ <ThemeProvider tone="default">
32
+ <AssistConnectorsOverlay connectors={connectors} />
33
+ </ThemeProvider>
34
+ </ConnectorsProvider>
35
+ </RunInstructionProvider>
36
+ </AiAssistanceConfigProvider>
37
+ )
38
+ }
@@ -0,0 +1,222 @@
1
+ import {Button, Dialog, Flex, Stack, Text, TextArea, Tooltip} from '@sanity/ui'
2
+ import {getInstructionTitle} from '../helpers/misc'
3
+ import {PlayIcon} from '@sanity/icons'
4
+ import {
5
+ createContext,
6
+ Dispatch,
7
+ FormEvent,
8
+ PropsWithChildren,
9
+ SetStateAction,
10
+ useCallback,
11
+ useContext,
12
+ useEffect,
13
+ useId,
14
+ useMemo,
15
+ useRef,
16
+ useState,
17
+ } from 'react'
18
+ import {UserInputBlock, userInputTypeName} from '../types'
19
+ import {RunInstructionArgs} from './AssistLayout'
20
+ import {useApiClient, useRunInstructionApi} from '../useApiClient'
21
+ import {useAiAssistanceConfig} from './AiAssistanceConfigContext'
22
+ import {FormFieldHeaderText} from 'sanity'
23
+
24
+ type BlockInputs = Record<string, string>
25
+ const NO_INPUT: BlockInputs = {}
26
+
27
+ export interface RunInstructionContextValue {
28
+ runInstruction: (req: RunInstructionArgs) => void
29
+ instructionLoading: boolean
30
+ }
31
+
32
+ export const RunInstructionContext = createContext<RunInstructionContextValue>({
33
+ runInstruction: () => {},
34
+ instructionLoading: false,
35
+ })
36
+
37
+ export function useRunInstruction() {
38
+ return useContext(RunInstructionContext)
39
+ }
40
+
41
+ function isUserInputBlock(block: {_type: string}): block is UserInputBlock {
42
+ return block._type === userInputTypeName
43
+ }
44
+
45
+ export function RunInstructionProvider(props: PropsWithChildren<{}>) {
46
+ const {config} = useAiAssistanceConfig()
47
+ const apiClient = useApiClient(config?.__customApiClient)
48
+ const {runInstruction: runInstructionRequest, loading} = useRunInstructionApi(apiClient)
49
+
50
+ const id = useId()
51
+
52
+ const [inputs, setInputs] = useState(NO_INPUT)
53
+ const [runRequest, setRunRequest] = useState<
54
+ (RunInstructionArgs & {userInputBlocks: UserInputBlock[]}) | undefined
55
+ >()
56
+
57
+ const runInstruction = useCallback(
58
+ (req: RunInstructionArgs) => {
59
+ if (loading) {
60
+ return
61
+ }
62
+ const {instruction, ...request} = req
63
+ const instructionKey = instruction._key
64
+ const userInputBlocks = instruction?.prompt
65
+ ?.flatMap((block) =>
66
+ block._type === 'block' ? block.children.filter(isUserInputBlock) : [block]
67
+ )
68
+ .filter(isUserInputBlock)
69
+
70
+ if (!userInputBlocks?.length) {
71
+ runInstructionRequest({
72
+ ...request,
73
+ instructionKey,
74
+ userTexts: undefined,
75
+ })
76
+ return
77
+ }
78
+
79
+ setRunRequest({
80
+ ...req,
81
+ userInputBlocks,
82
+ })
83
+ },
84
+ [setRunRequest, runInstructionRequest, loading]
85
+ )
86
+
87
+ const close = useCallback(() => {
88
+ setRunRequest(undefined)
89
+ setInputs(NO_INPUT)
90
+ }, [setRunRequest, setInputs])
91
+
92
+ const runWithInput = useCallback(() => {
93
+ if (runRequest) {
94
+ const {instruction, userTexts, ...request} = runRequest
95
+ runInstructionRequest({
96
+ ...request,
97
+ instructionKey: instruction._key,
98
+ userTexts: Object.entries(inputs).map(([key, value]) => ({
99
+ blockKey: key,
100
+ userInput: value,
101
+ })),
102
+ })
103
+ }
104
+ close()
105
+ }, [close, runInstructionRequest, runRequest, inputs])
106
+
107
+ const open = !!runRequest
108
+
109
+ const runDisabled = useMemo(
110
+ () =>
111
+ (runRequest?.userInputBlocks?.length ?? 0) >
112
+ Object.entries(inputs).filter(([, value]) => !!value).length,
113
+ [runRequest?.userInputBlocks, inputs]
114
+ )
115
+
116
+ const runButton = (
117
+ <Button
118
+ text="Run instruction"
119
+ onClick={runWithInput}
120
+ tone="primary"
121
+ icon={PlayIcon}
122
+ style={{width: '100%'}}
123
+ disabled={runDisabled}
124
+ />
125
+ )
126
+
127
+ const contextValue: RunInstructionContextValue = useMemo(
128
+ () => ({runInstruction, instructionLoading: loading}),
129
+ [runInstruction, loading]
130
+ )
131
+
132
+ return (
133
+ <RunInstructionContext.Provider value={contextValue}>
134
+ {open ? (
135
+ <Dialog
136
+ id={id}
137
+ open={open}
138
+ onClose={close}
139
+ width={1}
140
+ header={getInstructionTitle(runRequest?.instruction)}
141
+ footer={
142
+ <Flex justify="space-between" padding={2} flex={1}>
143
+ {runDisabled ? (
144
+ <Tooltip
145
+ content={
146
+ <Flex padding={2}>
147
+ <Text>Unable to run instruction. All fields must have a value.</Text>
148
+ </Flex>
149
+ }
150
+ placement="top"
151
+ >
152
+ <Flex flex={1}>{runButton}</Flex>
153
+ </Tooltip>
154
+ ) : (
155
+ runButton
156
+ )}
157
+ </Flex>
158
+ }
159
+ >
160
+ <Stack padding={4} space={2}>
161
+ {runRequest?.userInputBlocks?.map((block, i) => (
162
+ <UserInput
163
+ key={block._key}
164
+ block={block}
165
+ autoFocus={i === 0}
166
+ inputs={inputs}
167
+ setInputs={setInputs}
168
+ />
169
+ ))}
170
+ </Stack>
171
+ </Dialog>
172
+ ) : null}
173
+ {props.children}
174
+ </RunInstructionContext.Provider>
175
+ )
176
+ }
177
+
178
+ function UserInput(props: {
179
+ block: UserInputBlock
180
+ inputs: BlockInputs
181
+ setInputs: Dispatch<SetStateAction<BlockInputs>>
182
+ autoFocus?: boolean
183
+ }) {
184
+ const {block, autoFocus, setInputs, inputs} = props
185
+ const key = block._key
186
+ const textAreaRef = useRef<HTMLTextAreaElement>(null)
187
+
188
+ const onChange = useCallback(
189
+ (e: FormEvent<HTMLTextAreaElement>) => {
190
+ setInputs((current) => ({
191
+ ...current,
192
+ [key]: (e.currentTarget ?? e.target).value,
193
+ }))
194
+ },
195
+ [key, setInputs]
196
+ )
197
+
198
+ const value = useMemo(() => inputs[key], [inputs, key])
199
+
200
+ useEffect(() => {
201
+ if (!autoFocus) {
202
+ return
203
+ }
204
+ setTimeout(() => textAreaRef.current?.focus(), 0)
205
+ }, [autoFocus])
206
+
207
+ return (
208
+ <Stack padding={2} space={3}>
209
+ <FormFieldHeaderText
210
+ title={block?.message ?? 'Provide more context'}
211
+ description={block.description}
212
+ />
213
+ <TextArea
214
+ ref={textAreaRef}
215
+ rows={4}
216
+ value={value}
217
+ onChange={onChange}
218
+ style={{resize: 'vertical'}}
219
+ />
220
+ </Stack>
221
+ )
222
+ }
@@ -0,0 +1,9 @@
1
+ import {Badge} from '@sanity/ui'
2
+
3
+ export function AssistFeatureBadge() {
4
+ return (
5
+ <Badge fontSize={0} style={{margin: '-2px 0'}} tone="primary">
6
+ Beta
7
+ </Badge>
8
+ )
9
+ }
@@ -0,0 +1,25 @@
1
+ import {motion} from 'framer-motion'
2
+ import {ReactElement, ReactNode} from 'react'
3
+
4
+ export function Delay({
5
+ children,
6
+ ms = 1000,
7
+ durationMs = 250,
8
+ }: {
9
+ children?: ReactNode
10
+ ms?: number
11
+ durationMs?: number
12
+ }): ReactElement {
13
+ return (
14
+ <motion.div
15
+ initial={{opacity: 0, scale: 0.75}}
16
+ animate={{opacity: 1, scale: 1}}
17
+ transition={{
18
+ delay: ms / 1000,
19
+ duration: durationMs / 1000,
20
+ }}
21
+ >
22
+ {children}
23
+ </motion.div>
24
+ )
25
+ }
@@ -0,0 +1,25 @@
1
+ import {ObjectInputProps} from 'sanity'
2
+ import {Box} from '@sanity/ui'
3
+ import {useEffect, useRef} from 'react'
4
+
5
+ export function HideReferenceChangedBannerInput(props: ObjectInputProps) {
6
+ const ref = useRef<HTMLDivElement>(null)
7
+
8
+ // hides "reference was changed" banner (it is incorrectly flashing because the pane handler does not support the way wie use the assist pane)
9
+ useEffect(() => {
10
+ const parent = ref.current?.closest('[data-testid="pane-content"]')
11
+ if (!parent) {
12
+ return
13
+ }
14
+ const style = document.createElement('style')
15
+ const parentId = `id-${Math.random()}`.replace('.', '-')
16
+ parent.id = parentId
17
+
18
+ style.innerText = `
19
+ #${parentId} [data-testid="reference-changed-banner"] { display: none; }
20
+ `
21
+ parent.prepend(style)
22
+ }, [ref])
23
+
24
+ return <Box ref={ref}>{props.renderDefault(props)}</Box>
25
+ }
@@ -0,0 +1,73 @@
1
+ import {InputProps, isArraySchemaType, PatchEvent, unset} from 'sanity'
2
+ import {ErrorInfo, PropsWithChildren, useCallback, useMemo, useState} from 'react'
3
+ import {Box, Button, Card, ErrorBoundary, Flex, Stack, Text} from '@sanity/ui'
4
+ import {isPortableTextArray} from '../helpers/typeUtils'
5
+ import styled from 'styled-components'
6
+
7
+ const WrapPreCard = styled(Card)`
8
+ & pre {
9
+ white-space: pre-wrap !important;
10
+ }
11
+ `
12
+
13
+ export function SafeValueInput(props: InputProps) {
14
+ return (
15
+ <ErrorWrapper onChange={props.onChange}>
16
+ <PteValueFixer {...props} />
17
+ </ErrorWrapper>
18
+ )
19
+ }
20
+
21
+ export function ErrorWrapper(
22
+ props: PropsWithChildren<{onChange: (patchEvent: PatchEvent) => void}>
23
+ ) {
24
+ const {onChange} = props
25
+ const [error, setError] = useState<Error | undefined>()
26
+
27
+ const catchError = useCallback(
28
+ (params: {error: Error; info: ErrorInfo}) => {
29
+ setError(params.error)
30
+ },
31
+ [setError]
32
+ )
33
+
34
+ const unsetValue = useCallback(() => onChange(PatchEvent.from(unset())), [onChange])
35
+ const dismiss = useCallback(() => setError(undefined), [])
36
+ const catcher = <ErrorBoundary onCatch={catchError}>{props.children}</ErrorBoundary>
37
+
38
+ return error ? (
39
+ <Card border tone="critical" padding={2} contentEditable={false}>
40
+ <Stack space={3}>
41
+ <Text muted weight="semibold">
42
+ An error occurred.
43
+ </Text>
44
+
45
+ <WrapPreCard flex={1} padding={2} tone="critical" border>
46
+ {catcher}
47
+ </WrapPreCard>
48
+
49
+ <Flex width="fill" flex={1} gap={3}>
50
+ <Box flex={1}>
51
+ <Button text="Dismiss" onClick={dismiss} tone="primary" />
52
+ </Box>
53
+ <Button text="Unset value" onClick={unsetValue} tone="critical" />
54
+ </Flex>
55
+ </Stack>
56
+ </Card>
57
+ ) : (
58
+ catcher
59
+ )
60
+ }
61
+
62
+ function PteValueFixer(props: InputProps) {
63
+ const isPortableText = useMemo(
64
+ () => isArraySchemaType(props.schemaType) && isPortableTextArray(props.schemaType),
65
+ [props.schemaType]
66
+ )
67
+ const value = props.value
68
+ if (isPortableText && value && !(value as any[]).length) {
69
+ return props.renderDefault({...props, value: undefined})
70
+ }
71
+
72
+ return props.renderDefault(props)
73
+ }