import React, { useCallback, useDeferredValue, useEffect, useRef } from "react"; import { Button, CircularProgress, CloseIcon, cls, focusedDisabled, IconButton, Menu, MenuItem, SendIcon, Separator, TextareaAutosize } from "@firecms/ui"; import { AIIcon, EntityStatus, isPropertyBuilder, PluginFormActionProps, PropertiesOrBuilders, Property, PropertyOrBuilder, stripCollectionPath, useLargeLayout, useTranslation, } from "@firecms/core"; import { useDataEnhancementController } from "./DataEnhancementControllerProvider"; import { SamplePrompt } from "../types/data_enhancement_controller"; export function FormEnhanceAction({ entityId, path, status, collection, disabled, formContext, openEntityMode }: PluginFormActionProps) { const largeLayout = useLargeLayout(); const storageKey = createLocalStorageKey(path, status); const [loading, setLoading] = React.useState(false); const dataEnhancementController = useDataEnhancementController(); const { t } = useTranslation(); const [samplePrompts, setSamplePrompts] = React.useState(undefined); const [instructions, setInstructions] = React.useState(""); const { suggestions, getSamplePrompts, onAnalyticsEvent } = dataEnhancementController; const loadingPrompts = useRef(false); const updateSuggestedPrompts = useCallback(async function updateSuggestedPrompts(instructions?: string) { if (loadingPrompts.current) return; loadingPrompts.current = true; const prompts = status === "new" ? (await getSamplePrompts(collection.singularName ?? collection.name, instructions)).prompts : getPromptsForExistingEntities(collection.properties, t); const recentPromptsFromStorage = getRecentPromptsFromStorage(storageKey); const recentPrompts = recentPromptsFromStorage.map(prompt => prompt.prompt); setSamplePrompts([...recentPromptsFromStorage, ...prompts.filter(p => !recentPrompts.includes(p.prompt))].slice(0, 5)); loadingPrompts.current = false; }, [collection.name, collection.singularName, getSamplePrompts, status]); const deferredValues = useDeferredValue(formContext?.values); // const enoughData = countStringCharacters(deferredValues, collection.properties) > 20; useEffect(() => { if (!samplePrompts) { setSamplePrompts(getRecentPromptsFromStorage(storageKey)); updateSuggestedPrompts().then(); } }, [samplePrompts, storageKey, updateSuggestedPrompts, instructions, status]); useEffect(() => { updateSuggestedPrompts().then(); }, [status]); const enhance = (prompt?: string) => { if (!entityId || !formContext?.values) return; setLoading(true); if (prompt) { addRecentPrompt(storageKey, prompt); setSamplePrompts([{ prompt, type: "recent" }, ...(samplePrompts ?? []).slice(0, 5)]); onAnalyticsEvent?.("de:autofill_with_prompt", { path, entityName: collection.singularName ?? collection.name, status }); } else { onAnalyticsEvent?.("de:autofill_click", { path, entityName: collection.singularName ?? collection.name, status }); } return dataEnhancementController.enhance({ entityId, values: formContext!.values, instructions: prompt, replaceValues: true }).finally(() => { setLoading(false); }); }; if (!dataEnhancementController?.enabled) return null; const hasSuggestions = Object.values(suggestions).filter(Boolean).length > 0; const disabledSuggestionActions = !hasSuggestions; const promptSuggestionsEnabled = (samplePrompts ?? []).length > 0 && instructions.length === 0; const noIdSet = !formContext?.entityId; function submit() { enhance(instructions); } return ( {!loading && } {loading && } {t("autofill")} }> { enhance(); }}> {t("autofill_current_content")} {samplePrompts?.map((samplePrompt, index) => { return { onAnalyticsEvent?.("de:sample_prompt_click", { path, entityName: collection.singularName ?? collection.name, promptType: samplePrompt.type }); setInstructions(samplePrompt.prompt); enhance(samplePrompt.prompt); }} >
{samplePrompt.prompt}
{samplePrompt.type === "recent" && { e.preventDefault(); e.stopPropagation(); removeRecentPrompt(storageKey, samplePrompt.prompt); setSamplePrompts((samplePrompts ?? []).filter(p => p.prompt !== samplePrompt.prompt)); }} size={"smallest"} > }
; })}
{ event.stopPropagation(); }} placeholder={noIdSet ? t("set_id_first") : t("provide_instructions")} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); submit(); } }} onChange={(e) => { if (noIdSet) return; setInstructions(e.target.value); }} /> { setInstructions(""); }} color={!instructions ? "primary" : undefined} disabled={loading || !instructions}> enhance(instructions)} size={"small"} color={!instructions ? "primary" : undefined} disabled={loading || !instructions}> {loading && } {!loading && }
); } export interface EnhanceDialogProps { open: boolean; onClose: () => void; selectReferences: () => void; loading: boolean; enhance: (instructions: string) => void; samplePrompts?: string[]; } function getPromptsForExistingEntities(properties: PropertiesOrBuilders, t: any): SamplePrompt[] { const multilineProperties = Object.values(properties).filter((p: PropertyOrBuilder) => { if (!p) return false; if (isPropertyBuilder(p)) { return false; } return p.dataType === "string" && (p.markdown || p.multiline); }); const multilinePrompt: Property | undefined = multilineProperties.length > 0 ? multilineProperties[Math.floor(Math.random() * multilineProperties.length)] as Property : undefined; const prompts = [ t("fill_missing_fields"), t("translate_missing_content") ]; if (multilinePrompt) { prompts.push(t("add_2_paragraphs_to", { property: multilinePrompt.name })); } return prompts.map(p => ({ prompt: p, type: "sample" })); } const createLocalStorageKey = (path: string, status: EntityStatus,) => { const statusString = status === "new" ? "new" : "existing"; return `data_enhancement::${statusString}::${stripCollectionPath(path)}`; }; const getRecentPromptsFromStorage = (storageKey: string): SamplePrompt[] => { const item = localStorage.getItem(storageKey); return item ? JSON.parse(item).map((e: string) => ({ prompt: e, type: "recent" })) : []; }; const addRecentPrompt = (storageKey: string, prompt: string) => { if (!prompt || prompt.trim().length === 0) { return; } const recentPrompts = getRecentPromptsFromStorage(storageKey); localStorage.setItem(storageKey, JSON.stringify([prompt, ...recentPrompts .map(e => e.prompt) .filter(e => e !== prompt) .slice(0, 5)])); }; const removeRecentPrompt = (storageKey: string, prompt: string) => { localStorage.setItem(storageKey, JSON.stringify(getRecentPromptsFromStorage(storageKey) .map(e => e.prompt) .filter(e => e !== prompt))); };