@sanity/assist 1.0.12 → 1.1.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/README.md CHANGED
@@ -200,6 +200,33 @@ but some common caveats to the field that you may run into using AI Assist are:
200
200
  * Timeouts: To be able to write structured content, we're using the largest language models - long-running results may time out or intermittently fail
201
201
  * Limited capacity: The underlying LLM APIs used by AI Assist are resource constrained
202
202
 
203
+ ## Other features
204
+
205
+ ### Caption generation
206
+ AI Assist can optionally generate captions for images. This has to be enabled on an image-type/field,
207
+ by setting the `options.captionField` on the image type, where `captionField` is the field name of a
208
+ custom string-field on the image object:
209
+
210
+ ```tsx
211
+ defineField({
212
+ type: 'image',
213
+ name: 'inlineImage',
214
+ title: 'Image',
215
+ fields: [
216
+ defineField({
217
+ type: 'string',
218
+ name: 'caption',
219
+ title: 'Caption',
220
+ }),
221
+ ],
222
+ options: {
223
+ captionField: 'caption',
224
+ },
225
+ }),
226
+ ```
227
+ This will add a "Generate caption" action to the configured field.
228
+ "Generate caption" action will automatically run whenever the image changes.
229
+
203
230
  ## License
204
231
 
205
232
  [MIT](LICENSE) © Sanity
package/dist/index.d.ts CHANGED
@@ -40,6 +40,7 @@ declare module 'sanity' {
40
40
  interface GeopointOptions extends AssistOptions {}
41
41
  interface ImageOptions extends AssistOptions {
42
42
  imagePromptField?: string
43
+ captionField?: string
43
44
  }
44
45
  interface NumberOptions extends AssistOptions {}
45
46
  interface ObjectOptions extends AssistOptions {}
package/dist/index.esm.js CHANGED
@@ -120,6 +120,20 @@ function isType(schemaType, typeName) {
120
120
  }
121
121
  return isType(schemaType.type, typeName);
122
122
  }
123
+ function isImage(schemaType) {
124
+ return isType(schemaType, "image");
125
+ }
126
+ function getCaptionFieldOption(schemaType) {
127
+ var _a;
128
+ if (!schemaType) {
129
+ return void 0;
130
+ }
131
+ const captionField = (_a = schemaType.options) == null ? void 0 : _a.captionField;
132
+ if (captionField) {
133
+ return captionField;
134
+ }
135
+ return getCaptionFieldOption(schemaType.type);
136
+ }
123
137
  function isSchemaAssistEnabled(type) {
124
138
  var _a, _b;
125
139
  return !((_b = (_a = type.options) == null ? void 0 : _a.aiWritingAssistance) == null ? void 0 : _b.exclude);
@@ -736,10 +750,11 @@ function InstructionTaskHistoryButton(props) {
736
750
  const titledTasks = useMemo(() => {
737
751
  var _a2;
738
752
  const t = (_a2 = tasks == null ? void 0 : tasks.filter(task => task.started && /* @__PURE__ */new Date().getTime() - new Date(task.started).getTime() < maxHistoryVisibilityMs).map(task => {
753
+ var _a3;
739
754
  const instruction = instructions == null ? void 0 : instructions.find(i => i._key === task.instructionKey);
740
755
  return {
741
756
  ...task,
742
- title: showTitles ? getInstructionTitle(instruction) : void 0,
757
+ title: showTitles ? (_a3 = task.title) != null ? _a3 : getInstructionTitle(instruction) : void 0,
743
758
  cancel: () => cancelRun(task._key)
744
759
  };
745
760
  })) != null ? _a2 : [];
@@ -893,6 +908,48 @@ function useApiClient(customApiClient) {
893
908
  });
894
909
  return useMemo(() => customApiClient ? customApiClient(client) : client, [client, customApiClient]);
895
910
  }
911
+ function useGenerateCaption(apiClient) {
912
+ const [loading, setLoading] = useState(false);
913
+ const user = useCurrentUser();
914
+ const schema = useSchema();
915
+ const types = useMemo(() => serializeSchema(schema, {
916
+ leanFormat: true
917
+ }), [schema]);
918
+ const toast = useToast();
919
+ const generateCaption = useCallback(_ref4 => {
920
+ let {
921
+ path,
922
+ documentId
923
+ } = _ref4;
924
+ setLoading(true);
925
+ return apiClient.request({
926
+ method: "POST",
927
+ url: "/assist/tasks/generate-caption/".concat(apiClient.config().dataset, "?projectId=").concat(apiClient.config().projectId),
928
+ body: {
929
+ path,
930
+ documentId,
931
+ types,
932
+ userId: user == null ? void 0 : user.id
933
+ }
934
+ }).catch(e => {
935
+ toast.push({
936
+ status: "error",
937
+ title: "Generate caption failed",
938
+ description: e.message
939
+ });
940
+ setLoading(false);
941
+ throw e;
942
+ }).finally(() => {
943
+ setTimeout(() => {
944
+ setLoading(false);
945
+ }, 2e3);
946
+ });
947
+ }, [setLoading, apiClient, toast, user, types]);
948
+ return useMemo(() => ({
949
+ generateCaption,
950
+ loading
951
+ }), [generateCaption, loading]);
952
+ }
896
953
  function useGetInstructStatus(apiClient) {
897
954
  const [loading, setLoading] = useState(true);
898
955
  const projectClient = useClient({
@@ -1093,8 +1150,8 @@ function RunInstructionProvider(props) {
1093
1150
  runInstructionRequest({
1094
1151
  ...request,
1095
1152
  instructionKey: instruction._key,
1096
- userTexts: Object.entries(inputs).map(_ref4 => {
1097
- let [key, value] = _ref4;
1153
+ userTexts: Object.entries(inputs).map(_ref5 => {
1154
+ let [key, value] = _ref5;
1098
1155
  return {
1099
1156
  blockKey: key,
1100
1157
  userInput: value
@@ -1107,8 +1164,8 @@ function RunInstructionProvider(props) {
1107
1164
  const open = !!runRequest;
1108
1165
  const runDisabled = useMemo(() => {
1109
1166
  var _a2, _b;
1110
- return ((_b = (_a2 = runRequest == null ? void 0 : runRequest.userInputBlocks) == null ? void 0 : _a2.length) != null ? _b : 0) > Object.entries(inputs).filter(_ref5 => {
1111
- let [, value] = _ref5;
1167
+ return ((_b = (_a2 = runRequest == null ? void 0 : runRequest.userInputBlocks) == null ? void 0 : _a2.length) != null ? _b : 0) > Object.entries(inputs).filter(_ref6 => {
1168
+ let [, value] = _ref6;
1112
1169
  return !!value;
1113
1170
  }).length;
1114
1171
  }, [runRequest == null ? void 0 : runRequest.userInputBlocks, inputs]);
@@ -1688,10 +1745,10 @@ const assistInspector = {
1688
1745
  showAsAction: false
1689
1746
  }),
1690
1747
  component: AssistInspectorWrapper,
1691
- onClose(_ref6) {
1748
+ onClose(_ref7) {
1692
1749
  let {
1693
1750
  params
1694
- } = _ref6;
1751
+ } = _ref7;
1695
1752
  return {
1696
1753
  params: typed({
1697
1754
  ...params,
@@ -1764,18 +1821,19 @@ var __template$3 = (cooked, raw) => __freeze$3(__defProp$3(cooked, "raw", {
1764
1821
  var _a$3, _b$1;
1765
1822
  const fadeIn = keyframes(_a$3 || (_a$3 = __template$3(["\n 0% {\n opacity: 0;\n transform: scale(0.75);\n }\n 40% {\n opacity: 0;\n transform: scale(0.75);\n }\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n"])));
1766
1823
  const FadeInDiv = styled.div(_b$1 || (_b$1 = __template$3(["\n animation-name: ", ";\n animation-timing-function: ease-in-out;\n"])), fadeIn);
1767
- function FadeInContent(_ref7) {
1824
+ const FadeInContent = forwardRef(function FadeInContent2(_ref8, ref) {
1768
1825
  let {
1769
1826
  children,
1770
1827
  durationMs = 250
1771
- } = _ref7;
1828
+ } = _ref8;
1772
1829
  return /* @__PURE__ */jsx(FadeInDiv, {
1830
+ ref,
1773
1831
  style: {
1774
1832
  animationDuration: "".concat(durationMs, "ms")
1775
1833
  },
1776
1834
  children
1777
1835
  });
1778
- }
1836
+ });
1779
1837
  const purple = {
1780
1838
  "50": {
1781
1839
  title: "Purple 50",
@@ -3062,13 +3120,13 @@ function useSelectedSchema(fieldPath, documentSchema) {
3062
3120
  return currentSchema;
3063
3121
  }, [documentSchema, fieldPath]);
3064
3122
  }
3065
- function FieldsInitializer(_ref8) {
3123
+ function FieldsInitializer(_ref9) {
3066
3124
  let {
3067
3125
  pathKey,
3068
3126
  activePath,
3069
3127
  fieldExists,
3070
3128
  onChange
3071
- } = _ref8;
3129
+ } = _ref9;
3072
3130
  const initialized = useRef(false);
3073
3131
  useEffect(() => {
3074
3132
  if (initialized.current || fieldExists || activePath || !pathKey) {
@@ -3240,8 +3298,8 @@ function IconInput(props) {
3240
3298
  onChange
3241
3299
  } = props;
3242
3300
  const id = useId();
3243
- const items = useMemo(() => Object.entries(icons).map(_ref9 => {
3244
- let [key, icon] = _ref9;
3301
+ const items = useMemo(() => Object.entries(icons).map(_ref10 => {
3302
+ let [key, icon] = _ref10;
3245
3303
  return /* @__PURE__ */jsx(IconItem, {
3246
3304
  iconKey: key,
3247
3305
  icon,
@@ -3269,12 +3327,12 @@ function IconInput(props) {
3269
3327
  }
3270
3328
  });
3271
3329
  }
3272
- function IconItem(_ref10) {
3330
+ function IconItem(_ref11) {
3273
3331
  let {
3274
3332
  icon,
3275
3333
  iconKey: key,
3276
3334
  onChange
3277
- } = _ref10;
3335
+ } = _ref11;
3278
3336
  const onClick = useCallback(() => onChange(set(key)), [onChange, key]);
3279
3337
  return /* @__PURE__ */jsx(MenuItem, {
3280
3338
  icon,
@@ -3285,8 +3343,8 @@ function IconItem(_ref10) {
3285
3343
  }
3286
3344
  function getIcon(iconName) {
3287
3345
  var _a, _b;
3288
- return (_b = (_a = Object.entries(icons).find(_ref11 => {
3289
- let [key] = _ref11;
3346
+ return (_b = (_a = Object.entries(icons).find(_ref12 => {
3347
+ let [key] = _ref12;
3290
3348
  return key === iconName;
3291
3349
  })) == null ? void 0 : _a[1]) != null ? _b : icons.sparkles;
3292
3350
  }
@@ -3540,11 +3598,11 @@ const contextDocumentSchema = defineType({
3540
3598
  title: "title",
3541
3599
  context: "context"
3542
3600
  },
3543
- prepare(_ref12) {
3601
+ prepare(_ref13) {
3544
3602
  let {
3545
3603
  title,
3546
3604
  context
3547
- } = _ref12;
3605
+ } = _ref13;
3548
3606
  var _a;
3549
3607
  const text = context == null ? void 0 : context.flatMap(block => block == null ? void 0 : block.children).flatMap(child => {
3550
3608
  var _a2;
@@ -3673,10 +3731,10 @@ const fieldReference = defineType({
3673
3731
  select: {
3674
3732
  path: "path"
3675
3733
  },
3676
- prepare(_ref13) {
3734
+ prepare(_ref14) {
3677
3735
  let {
3678
3736
  path
3679
- } = _ref13;
3737
+ } = _ref14;
3680
3738
  return {
3681
3739
  title: path,
3682
3740
  path,
@@ -3822,12 +3880,12 @@ const instruction = defineType({
3822
3880
  title: "title",
3823
3881
  userId: "userId"
3824
3882
  },
3825
- prepare: _ref14 => {
3883
+ prepare: _ref15 => {
3826
3884
  let {
3827
3885
  icon,
3828
3886
  title,
3829
3887
  userId
3830
- } = _ref14;
3888
+ } = _ref15;
3831
3889
  return {
3832
3890
  title,
3833
3891
  icon: icon ? icons[icon] : SparklesIcon,
@@ -4115,6 +4173,100 @@ function PrivateIcon() {
4115
4173
  children: /* @__PURE__ */jsx(LockIcon, {})
4116
4174
  });
4117
4175
  }
4176
+ const ImageContext = createContext(void 0);
4177
+ function ImageContextProvider(props) {
4178
+ var _a;
4179
+ const {
4180
+ schemaType,
4181
+ path,
4182
+ value
4183
+ } = props;
4184
+ const assetRef = (_a = value == null ? void 0 : value.asset) == null ? void 0 : _a._ref;
4185
+ const [assetRefState, setAssetRefState] = useState(assetRef);
4186
+ const {
4187
+ documentId
4188
+ } = useAssistDocumentContext();
4189
+ const {
4190
+ config
4191
+ } = useAiAssistanceConfig();
4192
+ const apiClient = useApiClient(config == null ? void 0 : config.__customApiClient);
4193
+ const {
4194
+ generateCaption
4195
+ } = useGenerateCaption(apiClient);
4196
+ useEffect(() => {
4197
+ const captionField = getCaptionFieldOption(schemaType);
4198
+ if (assetRef && documentId && captionField && assetRef !== assetRefState) {
4199
+ setAssetRefState(assetRef);
4200
+ generateCaption({
4201
+ path: pathToString([...path, captionField]),
4202
+ documentId
4203
+ });
4204
+ }
4205
+ }, [schemaType, path, assetRef, assetRefState, documentId, generateCaption]);
4206
+ const context = useMemo(() => {
4207
+ const captionField = getCaptionFieldOption(schemaType);
4208
+ return captionField ? {
4209
+ captionPath: pathToString([...path, captionField]),
4210
+ assetRef
4211
+ } : void 0;
4212
+ }, [schemaType, path, assetRef]);
4213
+ return /* @__PURE__ */jsx(ImageContext.Provider, {
4214
+ value: context,
4215
+ children: props.renderDefault(props)
4216
+ });
4217
+ }
4218
+ function node$1(node2) {
4219
+ return node2;
4220
+ }
4221
+ const generateCaptionsActions = {
4222
+ name: "sanity-assist-generate-captions",
4223
+ useAction(props) {
4224
+ const pathKey = usePathKey(props.path);
4225
+ const {
4226
+ config
4227
+ } = useAiAssistanceConfig();
4228
+ const apiClient = useApiClient(config == null ? void 0 : config.__customApiClient);
4229
+ const {
4230
+ generateCaption,
4231
+ loading
4232
+ } = useGenerateCaption(apiClient);
4233
+ const imageContext = useContext(ImageContext);
4234
+ if (imageContext && pathKey === (imageContext == null ? void 0 : imageContext.captionPath)) {
4235
+ const {
4236
+ documentId
4237
+ } = useAssistDocumentContext();
4238
+ return useMemo(() => {
4239
+ return node$1({
4240
+ type: "action",
4241
+ icon: loading ? () => /* @__PURE__ */jsx(Box, {
4242
+ style: {
4243
+ height: 17
4244
+ },
4245
+ children: /* @__PURE__ */jsx(Spinner, {
4246
+ style: {
4247
+ transform: "translateY(6px)"
4248
+ }
4249
+ })
4250
+ }) : ImageIcon,
4251
+ title: "Generate caption",
4252
+ onAction: () => {
4253
+ if (loading) {
4254
+ return;
4255
+ }
4256
+ generateCaption({
4257
+ path: pathKey,
4258
+ documentId: documentId != null ? documentId : ""
4259
+ });
4260
+ },
4261
+ renderAsButton: true,
4262
+ disabled: loading,
4263
+ hidden: !imageContext.assetRef
4264
+ });
4265
+ }, [generateCaption, pathKey, documentId, loading, imageContext]);
4266
+ }
4267
+ return void 0;
4268
+ }
4269
+ };
4118
4270
  function node(node2) {
4119
4271
  return node2;
4120
4272
  }
@@ -4164,6 +4316,7 @@ const assistFieldActions = {
4164
4316
  const isInspectorOpen = (inspector == null ? void 0 : inspector.name) === aiInspectorId;
4165
4317
  const isPathSelected = pathKey === selectedPath;
4166
4318
  const isSelected = isInspectorOpen && isPathSelected;
4319
+ const imageCaptionAction = generateCaptionsActions.useAction(props);
4167
4320
  const manageInstructions = useCallback(() => isSelected ? closeInspector(aiInspectorId) : openInspector(aiInspectorId, {
4168
4321
  [fieldPathParam]: pathKey,
4169
4322
  [instructionParam]: void 0
@@ -4193,17 +4346,17 @@ const assistFieldActions = {
4193
4346
  type: "group",
4194
4347
  icon: () => null,
4195
4348
  title: "Run instructions",
4196
- children: instructions.map(instruction => instructionItem({
4349
+ children: [...instructions.map(instruction => instructionItem({
4197
4350
  instruction,
4198
4351
  isPrivate: Boolean(instruction.userId && instruction.userId === (currentUser == null ? void 0 : currentUser.id)),
4199
4352
  onInstructionAction,
4200
4353
  hidden: isHidden,
4201
4354
  documentIsNew: !!documentIsNew,
4202
4355
  assistSupported
4203
- })),
4356
+ })), imageCaptionAction].filter(Boolean),
4204
4357
  expanded: true
4205
4358
  }) : void 0;
4206
- }, [instructions, currentUser == null ? void 0 : currentUser.id, onInstructionAction, isHidden, documentIsNew, assistSupported]);
4359
+ }, [instructions, currentUser == null ? void 0 : currentUser.id, onInstructionAction, isHidden, documentIsNew, assistSupported, imageCaptionAction]);
4207
4360
  const instructionsLength = (instructions == null ? void 0 : instructions.length) || 0;
4208
4361
  const manageInstructionsItem = useMemo(() => node({
4209
4362
  type: "action",
@@ -4242,7 +4395,7 @@ const assistFieldActions = {
4242
4395
  title: pluginTitleShort,
4243
4396
  selected: isSelected
4244
4397
  }), [assistSupported, manageInstructions, isSelected]);
4245
- if (instructionsLength === 0) {
4398
+ if (instructionsLength === 0 && !imageCaptionAction) {
4246
4399
  return emptyAction;
4247
4400
  }
4248
4401
  return group;
@@ -4302,13 +4455,13 @@ function useInstructionToaster(documentId, documentSchemaType) {
4302
4455
  return !prevTask && task.ended || !(prevTask == null ? void 0 : prevTask.ended) && task.ended;
4303
4456
  }).filter(task => task.ended && isAfter(addSeconds(new Date(task.ended), 30), /* @__PURE__ */new Date()));
4304
4457
  endedTasks == null ? void 0 : endedTasks.forEach(task => {
4305
- var _a2;
4306
- const title = getInstructionTitle(task.instruction);
4458
+ var _a2, _b;
4459
+ const title = (_a2 = task.title) != null ? _a2 : getInstructionTitle(task.instruction);
4307
4460
  if (task.reason === "error") {
4308
4461
  toast.push({
4309
4462
  title: "Failed: ".concat(title),
4310
4463
  status: "error",
4311
- description: "Instruction failed. ".concat((_a2 = task.message) != null ? _a2 : ""),
4464
+ description: "Instruction failed. ".concat((_b = task.message) != null ? _b : ""),
4312
4465
  closable: true,
4313
4466
  duration: 1e4
4314
4467
  });
@@ -4355,11 +4508,11 @@ function AssistDocumentInputWrapper(props) {
4355
4508
  documentId
4356
4509
  });
4357
4510
  }
4358
- function AssistDocumentInput(_ref15) {
4511
+ function AssistDocumentInput(_ref16) {
4359
4512
  let {
4360
4513
  documentId,
4361
4514
  ...props
4362
- } = _ref15;
4515
+ } = _ref16;
4363
4516
  useInstructionToaster(documentId, props.schemaType);
4364
4517
  return /* @__PURE__ */jsx(FirstAssistedPathProvider, {
4365
4518
  members: props.members,
@@ -4448,12 +4601,12 @@ const assist = definePlugin(config => {
4448
4601
  unstable_fieldActions: prev => {
4449
4602
  return [...prev, assistFieldActions];
4450
4603
  },
4451
- unstable_languageFilter: (prev, _ref16) => {
4604
+ unstable_languageFilter: (prev, _ref17) => {
4452
4605
  let {
4453
4606
  documentId,
4454
4607
  schema,
4455
4608
  schemaType
4456
- } = _ref16;
4609
+ } = _ref17;
4457
4610
  const docSchema = schema.get(schemaType);
4458
4611
  return [...prev, createAssistDocumentPresence(documentId, docSchema)];
4459
4612
  }
@@ -4484,6 +4637,23 @@ const assist = definePlugin(config => {
4484
4637
  input: SafeValueInput
4485
4638
  }
4486
4639
  }
4640
+ })(), definePlugin({
4641
+ name: "".concat(packageName, "/generate-caption"),
4642
+ form: {
4643
+ components: {
4644
+ input: props => {
4645
+ const {
4646
+ schemaType
4647
+ } = props;
4648
+ if (isImage(schemaType)) {
4649
+ return /* @__PURE__ */jsx(ImageContextProvider, {
4650
+ ...props
4651
+ });
4652
+ }
4653
+ return props.renderDefault(props);
4654
+ }
4655
+ }
4656
+ }
4487
4657
  })()]
4488
4658
  };
4489
4659
  });