@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,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,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
|
+
}
|