@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,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,6 @@
1
+ import {useEditState} from 'sanity'
2
+
3
+ export function useDocumentState<T>(id: string, docType: string): T | undefined {
4
+ const state = useEditState(id, docType)
5
+ return (state.draft || state.published) as T | undefined
6
+ }
@@ -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
+ }