@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,87 @@
|
|
|
1
|
+
import {FieldError, FieldMember, ObjectInputMember, ObjectInputProps} from 'sanity'
|
|
2
|
+
import {findFieldMember, findFieldsetMember} from '../helpers'
|
|
3
|
+
import {Box, Flex, Stack, Text} from '@sanity/ui'
|
|
4
|
+
import {useId, useMemo} from 'react'
|
|
5
|
+
|
|
6
|
+
export function InstructionInput(props: ObjectInputProps) {
|
|
7
|
+
return (
|
|
8
|
+
<Stack space={[4, 4, 4, 5]}>
|
|
9
|
+
<NameField {...props} />
|
|
10
|
+
<ShareField {...props} />
|
|
11
|
+
<PromptField {...props} />
|
|
12
|
+
</Stack>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function PromptField(props: ObjectInputProps) {
|
|
17
|
+
const promptMember = findFieldMember(props.members, 'prompt')
|
|
18
|
+
return promptMember ? <ObjectInputMember {...props} member={promptMember} /> : null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const NONE: (FieldMember | FieldError)[] = []
|
|
22
|
+
|
|
23
|
+
function NameField(props: ObjectInputProps) {
|
|
24
|
+
const fieldsetMember = findFieldsetMember(props.members, 'appearance')
|
|
25
|
+
const titleId = useId()
|
|
26
|
+
|
|
27
|
+
const members = fieldsetMember?.fieldSet.members ?? NONE
|
|
28
|
+
const iconMember = findFieldMember(members, 'icon')
|
|
29
|
+
const titleMember = findFieldMember(members, 'title')
|
|
30
|
+
|
|
31
|
+
const titlePlaceholder = 'Untitled'
|
|
32
|
+
const moddedTitleMember = useMemo(() => {
|
|
33
|
+
if (!titleMember) {
|
|
34
|
+
return undefined
|
|
35
|
+
}
|
|
36
|
+
if (titleMember.kind === 'error') {
|
|
37
|
+
return titleMember
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
...titleMember,
|
|
41
|
+
field: {
|
|
42
|
+
...titleMember?.field,
|
|
43
|
+
schemaType: {
|
|
44
|
+
...titleMember?.field.schemaType,
|
|
45
|
+
placeholder: titlePlaceholder,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}, [titleMember, titlePlaceholder])
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Stack space={5}>
|
|
53
|
+
<Stack space={2}>
|
|
54
|
+
<Flex gap={1}>
|
|
55
|
+
<Text as="label" weight="semibold" size={1} htmlFor={titleId}>
|
|
56
|
+
Name
|
|
57
|
+
</Text>
|
|
58
|
+
</Flex>
|
|
59
|
+
|
|
60
|
+
<Text muted size={1}>
|
|
61
|
+
How this instruction appears in menus
|
|
62
|
+
</Text>
|
|
63
|
+
|
|
64
|
+
<Flex align="center">
|
|
65
|
+
{iconMember && (
|
|
66
|
+
<Box flex="none">
|
|
67
|
+
<ObjectInputMember {...props} member={iconMember} />
|
|
68
|
+
</Box>
|
|
69
|
+
)}
|
|
70
|
+
{moddedTitleMember && (
|
|
71
|
+
<Box flex={1} style={{marginLeft: -1}}>
|
|
72
|
+
<ObjectInputMember {...props} member={moddedTitleMember} />
|
|
73
|
+
</Box>
|
|
74
|
+
)}
|
|
75
|
+
</Flex>
|
|
76
|
+
</Stack>
|
|
77
|
+
</Stack>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ShareField(props: ObjectInputProps) {
|
|
82
|
+
const fieldsetMember = findFieldsetMember(props.members, 'appearance')
|
|
83
|
+
const members = fieldsetMember?.fieldSet.members ?? NONE
|
|
84
|
+
const visibilityMember = findFieldMember(members, 'userId')
|
|
85
|
+
|
|
86
|
+
return <>{visibilityMember && <ObjectInputMember {...props} member={visibilityMember} />}</>
|
|
87
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {ArrayOfObjectsInputProps, set, typed} from 'sanity'
|
|
2
|
+
import {Box} from '@sanity/ui'
|
|
3
|
+
import styled from 'styled-components'
|
|
4
|
+
import {useEffect} from 'react'
|
|
5
|
+
import {ContextBlock, FieldRef, PromptBlock, PromptTextBlock, UserInputBlock} from '../../../types'
|
|
6
|
+
import {randomKey} from '../../../_lib/randomKey'
|
|
7
|
+
|
|
8
|
+
const PteMods = styled(Box)`
|
|
9
|
+
& [data-testid='pt-editor__toolbar-card'] > div > div:last-child {
|
|
10
|
+
display: none;
|
|
11
|
+
}
|
|
12
|
+
& [data-testid='pt-editor'] {
|
|
13
|
+
min-height: 300px;
|
|
14
|
+
}
|
|
15
|
+
& [data-testid='pt-editor'] .pt-inline-object * {
|
|
16
|
+
max-width: 400px;
|
|
17
|
+
}
|
|
18
|
+
`
|
|
19
|
+
|
|
20
|
+
export function PromptInput(props: ArrayOfObjectsInputProps) {
|
|
21
|
+
// quickfixes the model (converts blocks to inline blocks for alpha customers)
|
|
22
|
+
// backend supports both types, but this prevents "missing block" schema errors
|
|
23
|
+
useOnlyInlineBlocks(props)
|
|
24
|
+
return <PteMods>{props.renderDefault(props)}</PteMods>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function useOnlyInlineBlocks(props: ArrayOfObjectsInputProps) {
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
let needsFix = false
|
|
30
|
+
const val = ((props.value as PromptBlock[]) ?? []).map((block) => {
|
|
31
|
+
if (block._type === 'block') {
|
|
32
|
+
return block
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
needsFix = true
|
|
36
|
+
return typed<PromptTextBlock>({
|
|
37
|
+
_key: randomKey(12),
|
|
38
|
+
_type: 'block',
|
|
39
|
+
level: 0,
|
|
40
|
+
markDefs: [],
|
|
41
|
+
style: 'normal',
|
|
42
|
+
children: [block as FieldRef | ContextBlock | UserInputBlock],
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
if (needsFix) {
|
|
47
|
+
props.onChange(set(val))
|
|
48
|
+
}
|
|
49
|
+
// only run this once when loading the field
|
|
50
|
+
// eslint-disable-next-line
|
|
51
|
+
}, [])
|
|
52
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {icons} from '@sanity/icons'
|
|
2
|
+
import {set, StringInputProps} from 'sanity'
|
|
3
|
+
import {Button, Menu, MenuButton, MenuItem} from '@sanity/ui'
|
|
4
|
+
import {ElementType, ReactNode, useCallback, useId, useMemo} from 'react'
|
|
5
|
+
|
|
6
|
+
export function IconInput(props: StringInputProps) {
|
|
7
|
+
const {value, onChange} = props
|
|
8
|
+
const id = useId()
|
|
9
|
+
const items = useMemo(
|
|
10
|
+
() =>
|
|
11
|
+
Object.entries(icons).map(([key, icon]) => (
|
|
12
|
+
<IconItem key={key} iconKey={key} icon={icon} onChange={onChange} />
|
|
13
|
+
)),
|
|
14
|
+
[onChange]
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
const selectedIcon = useMemo(() => getIcon(value), [value])
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<MenuButton
|
|
21
|
+
button={
|
|
22
|
+
<Button icon={selectedIcon} title="Select icon" padding={3} mode="ghost" radius={1} />
|
|
23
|
+
}
|
|
24
|
+
id={id}
|
|
25
|
+
menu={<Menu style={{maxHeight: 300}}>{items}</Menu>}
|
|
26
|
+
popover={{portal: true}}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function IconItem({
|
|
32
|
+
icon,
|
|
33
|
+
iconKey: key,
|
|
34
|
+
onChange,
|
|
35
|
+
}: {
|
|
36
|
+
iconKey: string
|
|
37
|
+
icon: ElementType | ReactNode
|
|
38
|
+
onChange: StringInputProps['onChange']
|
|
39
|
+
}) {
|
|
40
|
+
const onClick = useCallback(() => onChange(set(key)), [onChange, key])
|
|
41
|
+
return <MenuItem icon={icon} title={key} text={key} onClick={onClick} />
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getIcon(iconName?: string) {
|
|
45
|
+
return Object.entries(icons).find(([key]) => key === iconName)?.[1] ?? icons.sparkles
|
|
46
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {set, StringInputProps, unset, useCurrentUser} from 'sanity'
|
|
2
|
+
import {Card, Flex, Switch, Text} from '@sanity/ui'
|
|
3
|
+
import {useCallback, useId} from 'react'
|
|
4
|
+
|
|
5
|
+
export function InstructionVisibility(props: StringInputProps) {
|
|
6
|
+
const {value, onChange} = props
|
|
7
|
+
|
|
8
|
+
const user = useCurrentUser()
|
|
9
|
+
|
|
10
|
+
const handleChange = useCallback(() => {
|
|
11
|
+
const newValue = value ? '' : user?.id ?? ''
|
|
12
|
+
onChange(newValue ? set(newValue) : unset())
|
|
13
|
+
}, [onChange, user, value])
|
|
14
|
+
|
|
15
|
+
const id = useId()
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Card>
|
|
19
|
+
<Flex gap={2} align="flex-start">
|
|
20
|
+
<div style={{margin: '-3px 0'}}>
|
|
21
|
+
<Switch
|
|
22
|
+
{...props.elementProps}
|
|
23
|
+
id={id}
|
|
24
|
+
value={`${!value}`}
|
|
25
|
+
checked={!value}
|
|
26
|
+
onChange={handleChange}
|
|
27
|
+
disabled={props.elementProps.readOnly}
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<Text muted size={1} weight="medium">
|
|
32
|
+
<label htmlFor={id}>Make visible to all Studio members</label>
|
|
33
|
+
</Text>
|
|
34
|
+
</Flex>
|
|
35
|
+
</Card>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {getPublishedId, ObjectSchemaType, useEditState} from 'sanity'
|
|
2
|
+
import {useStudioAssistDocument} from './useStudioAssistDocument'
|
|
3
|
+
import {AssistDocumentContextValue} from '../AssistDocumentContext'
|
|
4
|
+
import {useMemo} from 'react'
|
|
5
|
+
import {isDocAssistable} from '../RequestRunInstructionProvider'
|
|
6
|
+
import {useDocumentPane} from 'sanity/desk'
|
|
7
|
+
import {useAiPaneRouter} from '../../assistInspector/helpers'
|
|
8
|
+
import {fieldPathParam} from '../../types'
|
|
9
|
+
|
|
10
|
+
export function useAssistDocumentContextValue(
|
|
11
|
+
documentId: string,
|
|
12
|
+
documentSchemaType: ObjectSchemaType
|
|
13
|
+
) {
|
|
14
|
+
const {published, draft} = useEditState(
|
|
15
|
+
getPublishedId(documentId),
|
|
16
|
+
documentSchemaType.name,
|
|
17
|
+
'low'
|
|
18
|
+
)
|
|
19
|
+
const assistableDocumentId = draft?._id || published?._id || documentId
|
|
20
|
+
const documentIsNew = Boolean(!draft?._id && !published?._id)
|
|
21
|
+
const documentIsAssistable = isDocAssistable(documentSchemaType, published, draft)
|
|
22
|
+
|
|
23
|
+
const {params} = useAiPaneRouter()
|
|
24
|
+
const selectedPath = params[fieldPathParam]
|
|
25
|
+
const {openInspector, closeInspector, inspector, onChange: documentOnChange} = useDocumentPane()
|
|
26
|
+
|
|
27
|
+
const assistDocument = useStudioAssistDocument({
|
|
28
|
+
documentId,
|
|
29
|
+
schemaType: documentSchemaType,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const value: AssistDocumentContextValue = useMemo(() => {
|
|
33
|
+
const base = {
|
|
34
|
+
documentId,
|
|
35
|
+
assistableDocumentId,
|
|
36
|
+
documentSchemaType,
|
|
37
|
+
documentIsNew,
|
|
38
|
+
documentIsAssistable,
|
|
39
|
+
openInspector,
|
|
40
|
+
closeInspector,
|
|
41
|
+
inspector,
|
|
42
|
+
documentOnChange,
|
|
43
|
+
selectedPath,
|
|
44
|
+
}
|
|
45
|
+
if (!assistDocument) {
|
|
46
|
+
return {...base, loading: true, assistDocument: undefined}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...base,
|
|
50
|
+
loading: false,
|
|
51
|
+
assistDocument: assistDocument,
|
|
52
|
+
}
|
|
53
|
+
}, [
|
|
54
|
+
assistDocument,
|
|
55
|
+
documentIsAssistable,
|
|
56
|
+
documentId,
|
|
57
|
+
assistableDocumentId,
|
|
58
|
+
documentSchemaType,
|
|
59
|
+
documentIsNew,
|
|
60
|
+
openInspector,
|
|
61
|
+
closeInspector,
|
|
62
|
+
inspector,
|
|
63
|
+
documentOnChange,
|
|
64
|
+
selectedPath,
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
return value
|
|
68
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {useEffect, useRef} from 'react'
|
|
2
|
+
import {InstructionTask} from '../../types'
|
|
3
|
+
import {addSeconds, isAfter} from 'date-fns'
|
|
4
|
+
import {getInstructionTitle} from '../../helpers/misc'
|
|
5
|
+
import {useStudioAssistDocument} from './useStudioAssistDocument'
|
|
6
|
+
import {ObjectSchemaType, useCurrentUser} from 'sanity'
|
|
7
|
+
import {useToast} from '@sanity/ui'
|
|
8
|
+
|
|
9
|
+
const NO_TASKS: InstructionTask[] = []
|
|
10
|
+
|
|
11
|
+
export function useInstructionToaster(documentId: string, documentSchemaType: ObjectSchemaType) {
|
|
12
|
+
const assistDocument = useStudioAssistDocument({documentId, schemaType: documentSchemaType})
|
|
13
|
+
|
|
14
|
+
const assistDocLoaded = !!assistDocument
|
|
15
|
+
const currentUser = useCurrentUser()
|
|
16
|
+
const toast = useToast()
|
|
17
|
+
const tasks = assistDocument?.tasks
|
|
18
|
+
const previousTasks = useRef<InstructionTask[] | undefined | 'initial'>('initial')
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (!assistDocLoaded) {
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (previousTasks.current !== 'initial') {
|
|
26
|
+
const prevTaskByKey = Object.fromEntries(
|
|
27
|
+
(previousTasks.current ?? NO_TASKS).map((run) => [run._key, run])
|
|
28
|
+
)
|
|
29
|
+
const endedTasks = tasks
|
|
30
|
+
?.filter((task) => task.startedByUserId === currentUser?.id)
|
|
31
|
+
.filter((task) => {
|
|
32
|
+
const prevTask = prevTaskByKey[task._key]
|
|
33
|
+
return (!prevTask && task.ended) || (!prevTask?.ended && task.ended)
|
|
34
|
+
})
|
|
35
|
+
// filter out old stuff
|
|
36
|
+
.filter((task) => task.ended && isAfter(addSeconds(new Date(task.ended), 30), new Date()))
|
|
37
|
+
|
|
38
|
+
endedTasks?.forEach((task) => {
|
|
39
|
+
const title = getInstructionTitle(task.instruction)
|
|
40
|
+
if (task.reason === 'error') {
|
|
41
|
+
toast.push({
|
|
42
|
+
title: `Failed: ${title}`,
|
|
43
|
+
status: 'error',
|
|
44
|
+
description: `Instruction failed. ${task.message ?? ''}`,
|
|
45
|
+
closable: true,
|
|
46
|
+
duration: 10000,
|
|
47
|
+
})
|
|
48
|
+
} else if (task.reason === 'timeout') {
|
|
49
|
+
toast.push({
|
|
50
|
+
title: `Timeout: ${title}`,
|
|
51
|
+
status: 'error',
|
|
52
|
+
description: `Instruction timed out.`,
|
|
53
|
+
closable: true,
|
|
54
|
+
})
|
|
55
|
+
} else if (task.reason === 'success') {
|
|
56
|
+
toast.push({
|
|
57
|
+
title: `Success: ${title}`,
|
|
58
|
+
status: 'success',
|
|
59
|
+
description: `Instruction completed.`,
|
|
60
|
+
closable: true,
|
|
61
|
+
})
|
|
62
|
+
} else if (task.reason === 'aborted') {
|
|
63
|
+
toast.push({
|
|
64
|
+
title: `Canceled: ${title}`,
|
|
65
|
+
status: 'warning',
|
|
66
|
+
description: `Instruction canceled.`,
|
|
67
|
+
closable: true,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
previousTasks.current = tasks
|
|
73
|
+
}, [tasks, previousTasks, toast, currentUser, assistDocLoaded])
|
|
74
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {useEffect, useMemo} from 'react'
|
|
2
|
+
import {
|
|
3
|
+
assistDocumentTypeName,
|
|
4
|
+
AssistTasksStatus,
|
|
5
|
+
assistTasksStatusTypeName,
|
|
6
|
+
FieldRef,
|
|
7
|
+
fieldReferenceTypeName,
|
|
8
|
+
InstructionTask,
|
|
9
|
+
StudioAssistDocument,
|
|
10
|
+
StudioAssistField,
|
|
11
|
+
StudioInstruction,
|
|
12
|
+
} from '../../types'
|
|
13
|
+
import {
|
|
14
|
+
ObjectSchemaType,
|
|
15
|
+
pathToString,
|
|
16
|
+
typed,
|
|
17
|
+
useClient,
|
|
18
|
+
useCurrentUser,
|
|
19
|
+
useValidationStatus,
|
|
20
|
+
ValidationMarker,
|
|
21
|
+
} from 'sanity'
|
|
22
|
+
import {useDocumentState} from './useDocumentState'
|
|
23
|
+
import {assistDocumentId, assistTasksStatusId, publicId} from '../../helpers/ids'
|
|
24
|
+
import {maxHistoryVisibilityMs} from '../../constants'
|
|
25
|
+
|
|
26
|
+
interface UseAssistDocumentProps {
|
|
27
|
+
documentId: string
|
|
28
|
+
schemaType: ObjectSchemaType
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useStudioAssistDocument({
|
|
32
|
+
documentId,
|
|
33
|
+
schemaType,
|
|
34
|
+
}: UseAssistDocumentProps): StudioAssistDocument | undefined {
|
|
35
|
+
const documentTypeName = schemaType.name
|
|
36
|
+
const currentUser = useCurrentUser()
|
|
37
|
+
|
|
38
|
+
const validation = useValidationStatus(publicId(documentId), schemaType.name).validation
|
|
39
|
+
const assistDocument = useDocumentState<StudioAssistDocument>(
|
|
40
|
+
assistDocumentId(documentTypeName),
|
|
41
|
+
assistDocumentTypeName
|
|
42
|
+
)
|
|
43
|
+
const assistTasksStatus = useDocumentState<AssistTasksStatus>(
|
|
44
|
+
assistTasksStatusId(documentId ?? ''),
|
|
45
|
+
assistTasksStatusTypeName
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
const client = useClient({apiVersion: '2023-01-01'})
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!assistDocument) {
|
|
52
|
+
client.createIfNotExists({
|
|
53
|
+
_id: assistDocumentId(documentTypeName),
|
|
54
|
+
_type: assistDocumentTypeName,
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}, [client, assistDocument, documentTypeName])
|
|
58
|
+
|
|
59
|
+
return useMemo(() => {
|
|
60
|
+
if (!assistDocument) {
|
|
61
|
+
return undefined
|
|
62
|
+
}
|
|
63
|
+
const tasks = assistTasksStatus?.tasks ?? []
|
|
64
|
+
const fields = (assistDocument?.fields ?? []).map((assistField): StudioAssistField => {
|
|
65
|
+
return {
|
|
66
|
+
...assistField,
|
|
67
|
+
tasks: tasks.filter((task) => task.path === assistField.path),
|
|
68
|
+
instructions: assistField.instructions
|
|
69
|
+
?.filter((p) => !p.userId || p.userId === currentUser?.id)
|
|
70
|
+
.map((instruction) => asStudioInstruction(instruction, tasks, validation)),
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
return typed<StudioAssistDocument>({
|
|
74
|
+
...assistDocument,
|
|
75
|
+
tasks: tasks?.map((task) => {
|
|
76
|
+
const instruction = fields
|
|
77
|
+
.find((f) => f.path === task.path)
|
|
78
|
+
?.instructions?.find((i) => i._key === task.instructionKey)
|
|
79
|
+
return {
|
|
80
|
+
...task,
|
|
81
|
+
instruction,
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
fields: fields,
|
|
85
|
+
})
|
|
86
|
+
}, [assistDocument, assistTasksStatus, currentUser, validation])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function asStudioInstruction(
|
|
90
|
+
instruction: StudioInstruction,
|
|
91
|
+
run: InstructionTask[],
|
|
92
|
+
validation: ValidationMarker[]
|
|
93
|
+
): StudioInstruction {
|
|
94
|
+
const errors = validation.filter((marker) => marker.level === 'error')
|
|
95
|
+
|
|
96
|
+
const fieldRefs: FieldRef[] = (instruction?.prompt ?? []).flatMap((block) => {
|
|
97
|
+
if (block._type === 'block') {
|
|
98
|
+
return block.children.filter((c): c is FieldRef => c._type === fieldReferenceTypeName)
|
|
99
|
+
}
|
|
100
|
+
return []
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...instruction,
|
|
105
|
+
tasks: run
|
|
106
|
+
.filter((task) => task.instructionKey === instruction._key)
|
|
107
|
+
.filter(
|
|
108
|
+
(task) =>
|
|
109
|
+
task.started &&
|
|
110
|
+
new Date().getTime() - new Date(task.started).getTime() < maxHistoryVisibilityMs
|
|
111
|
+
),
|
|
112
|
+
validation: errors.filter((marker) =>
|
|
113
|
+
fieldRefs
|
|
114
|
+
.map((r) => r.path)
|
|
115
|
+
.filter((p): p is string => !!p)
|
|
116
|
+
.find((path) => pathToString(marker.path) === path)
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './AssistDocumentContextProvider'
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {FieldProps, isArraySchemaType} from 'sanity'
|
|
2
|
+
import {useAssistPresence} from '../presence/useAssistPresence'
|
|
3
|
+
import {useMemo} from 'react'
|
|
4
|
+
import {Box, Flex} from '@sanity/ui'
|
|
5
|
+
import {contextDocumentTypeName} from '../types'
|
|
6
|
+
import {isAssistSupported} from '../helpers/assistSupported'
|
|
7
|
+
import {isPortableTextArray, isType} from '../helpers/typeUtils'
|
|
8
|
+
import {AiFieldPresence} from '../presence/AiFieldPresence'
|
|
9
|
+
import {FieldActionsOnboarding} from '../onboarding/FieldActionsOnboarding'
|
|
10
|
+
|
|
11
|
+
export function AssistFieldWrapper(props: FieldProps) {
|
|
12
|
+
const {schemaType} = props
|
|
13
|
+
|
|
14
|
+
const isSupported = useMemo(() => isAssistSupported(schemaType), [schemaType])
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
!isSupported ||
|
|
18
|
+
schemaType.name.startsWith('sanity.') ||
|
|
19
|
+
schemaType.name === contextDocumentTypeName
|
|
20
|
+
) {
|
|
21
|
+
return props.renderDefault(props)
|
|
22
|
+
}
|
|
23
|
+
if (!isType(props.schemaType, 'document') && props.inputId !== 'root') {
|
|
24
|
+
return <AssistField {...props}>{props.children}</AssistField>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return props.renderDefault(props)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function AssistField(props: FieldProps) {
|
|
31
|
+
const isPortableText = useMemo(
|
|
32
|
+
() => !!(isArraySchemaType(props.schemaType) && isPortableTextArray(props.schemaType)),
|
|
33
|
+
[props.schemaType]
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const presence = useAssistPresence(props.path, isPortableText)
|
|
37
|
+
|
|
38
|
+
const actions = (
|
|
39
|
+
<Flex gap={2} align="center" justify="space-between">
|
|
40
|
+
{presence.map((pre) => (
|
|
41
|
+
<Box key={pre.user.id}>
|
|
42
|
+
<AiFieldPresence key={pre.lastActiveAt} presence={pre} />
|
|
43
|
+
</Box>
|
|
44
|
+
))}
|
|
45
|
+
|
|
46
|
+
<FieldActionsOnboarding {...props}>{props.actions}</FieldActionsOnboarding>
|
|
47
|
+
</Flex>
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return props.renderDefault({...props, actions})
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {BlockProps, PatchEvent, useFormCallbacks} from 'sanity'
|
|
2
|
+
import {useAssistPresence} from '../presence/useAssistPresence'
|
|
3
|
+
import {Box, Flex} from '@sanity/ui'
|
|
4
|
+
import {ErrorWrapper} from '../components/SafeValueInput'
|
|
5
|
+
import {useCallback} from 'react'
|
|
6
|
+
import {AiFieldPresence} from '../presence/AiFieldPresence'
|
|
7
|
+
|
|
8
|
+
export function AssistFormBlock(props: BlockProps) {
|
|
9
|
+
const presence = useAssistPresence(props.path, true)
|
|
10
|
+
const {onChange} = useFormCallbacks()
|
|
11
|
+
const key = props.value._key
|
|
12
|
+
const localOnChange = useCallback(
|
|
13
|
+
(patchEvent: PatchEvent) => {
|
|
14
|
+
if (!key) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
onChange(PatchEvent.from(patchEvent).prefixAll({_key: key}))
|
|
18
|
+
},
|
|
19
|
+
[onChange, key]
|
|
20
|
+
)
|
|
21
|
+
return (
|
|
22
|
+
<ErrorWrapper onChange={localOnChange}>
|
|
23
|
+
<Flex align="center" justify="space-between">
|
|
24
|
+
<Box flex={1}>{props.renderDefault(props)}</Box>
|
|
25
|
+
{presence.map((pre) => (
|
|
26
|
+
<AiFieldPresence key={pre.lastActiveAt} presence={pre} />
|
|
27
|
+
))}
|
|
28
|
+
</Flex>
|
|
29
|
+
</ErrorWrapper>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import {BlockProps} from 'sanity'
|
|
2
|
+
import {Box} from '@sanity/ui'
|
|
3
|
+
import {createContext} from 'react'
|
|
4
|
+
|
|
5
|
+
// workaround for preview value sometimes lagging behind
|
|
6
|
+
export const InlineBlockValueContext = createContext<unknown>(undefined)
|
|
7
|
+
|
|
8
|
+
export function AssistInlineFormBlock(props: BlockProps) {
|
|
9
|
+
return (
|
|
10
|
+
<InlineBlockValueContext.Provider value={props.value}>
|
|
11
|
+
<Box flex={1}>{props.renderDefault(props)}</Box>
|
|
12
|
+
</InlineBlockValueContext.Provider>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {ItemProps} from 'sanity'
|
|
2
|
+
import {useAssistPresence} from '../presence/useAssistPresence'
|
|
3
|
+
import {Box, Flex} from '@sanity/ui'
|
|
4
|
+
import {AiFieldPresence} from '../presence/AiFieldPresence'
|
|
5
|
+
|
|
6
|
+
export function AssistItem(props: ItemProps) {
|
|
7
|
+
const {path} = props
|
|
8
|
+
const presence = useAssistPresence(path, true)
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<Flex align="center" width="fill" style={{position: 'relative'}}>
|
|
12
|
+
<Box flex={1}>{props.renderDefault({...props})}</Box>
|
|
13
|
+
{presence.map((pre) => (
|
|
14
|
+
<Box key={pre.user.id} style={{position: 'absolute', right: 35}}>
|
|
15
|
+
<AiFieldPresence presence={pre} />
|
|
16
|
+
</Box>
|
|
17
|
+
))}
|
|
18
|
+
</Flex>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {Path, ValidationMarker} from 'sanity'
|
|
2
|
+
import {useCallback} from 'react'
|
|
3
|
+
import {Box, ButtonTone, Flex, MenuItem, Stack, Text} from '@sanity/ui'
|
|
4
|
+
import styled from 'styled-components'
|
|
5
|
+
|
|
6
|
+
interface ValidationListItemProps {
|
|
7
|
+
marker: ValidationMarker
|
|
8
|
+
onClick?: (path?: Path) => void
|
|
9
|
+
path: string
|
|
10
|
+
truncate?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const StyledText = styled(Text)`
|
|
14
|
+
white-space: initial;
|
|
15
|
+
`
|
|
16
|
+
|
|
17
|
+
const MENU_ITEM_TONES: Record<'error' | 'warning' | 'info', ButtonTone> = {
|
|
18
|
+
error: 'critical',
|
|
19
|
+
warning: 'caution',
|
|
20
|
+
info: 'primary',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ListItem(props: ValidationListItemProps) {
|
|
24
|
+
const {marker, onClick, path, truncate} = props
|
|
25
|
+
|
|
26
|
+
const handleClick = useCallback(() => {
|
|
27
|
+
if (onClick) {
|
|
28
|
+
onClick(marker.path)
|
|
29
|
+
}
|
|
30
|
+
}, [marker.path, onClick])
|
|
31
|
+
|
|
32
|
+
const menuItemTone = MENU_ITEM_TONES[marker?.level] || undefined
|
|
33
|
+
|
|
34
|
+
const children = (
|
|
35
|
+
<Flex>
|
|
36
|
+
{/* <Box>
|
|
37
|
+
<Text size={1}>
|
|
38
|
+
{marker.level === 'error' && <ErrorOutlineIcon />}
|
|
39
|
+
{marker.level === 'warning' && <WarningOutlineIcon />}
|
|
40
|
+
{marker.level === 'info' && <InfoOutlineIcon />}
|
|
41
|
+
</Text>
|
|
42
|
+
</Box>*/}
|
|
43
|
+
|
|
44
|
+
<Stack space={2} flex={1}>
|
|
45
|
+
{path && (
|
|
46
|
+
<StyledText size={1} weight="semibold">
|
|
47
|
+
{path}
|
|
48
|
+
</StyledText>
|
|
49
|
+
)}
|
|
50
|
+
{marker.item.message && (
|
|
51
|
+
<StyledText muted size={1} textOverflow={truncate ? 'ellipsis' : undefined}>
|
|
52
|
+
{marker.item.message}
|
|
53
|
+
</StyledText>
|
|
54
|
+
)}
|
|
55
|
+
</Stack>
|
|
56
|
+
</Flex>
|
|
57
|
+
)
|
|
58
|
+
return (
|
|
59
|
+
<MenuItem padding={1} onClick={handleClick} radius={2} tone={menuItemTone}>
|
|
60
|
+
<Box padding={2}>{children}</Box>
|
|
61
|
+
</MenuItem>
|
|
62
|
+
)
|
|
63
|
+
}
|