diff --git a/packages/nc-gui/components/ai/PromptWithFields.vue b/packages/nc-gui/components/ai/PromptWithFields.vue index 3b56693836..219f6cd597 100644 --- a/packages/nc-gui/components/ai/PromptWithFields.vue +++ b/packages/nc-gui/components/ai/PromptWithFields.vue @@ -3,6 +3,7 @@ import Placeholder from '@tiptap/extension-placeholder' import StarterKit from '@tiptap/starter-kit' import Mention from '@tiptap/extension-mention' import { EditorContent, useEditor } from '@tiptap/vue-3' +import tippy from 'tippy.js' import { type ColumnType, UITypes } from 'nocodb-sdk' import FieldList from '~/helpers/tiptapExtensions/mention/FieldList' import suggestion from '~/helpers/tiptapExtensions/mention/suggestion.ts' @@ -15,6 +16,7 @@ const props = withDefaults( promptFieldTagClassName?: string suggestionIconClassName?: string placeholder?: string + readOnly?: boolean }>(), { options: () => [], @@ -26,6 +28,7 @@ const props = withDefaults( * @example: :placeholder="`Enter prompt here...\n\neg : Categorise this {Notes}`" */ placeholder: 'Write your prompt here...', + readOnly: false, }, ) @@ -38,7 +41,9 @@ const vModel = computed({ }, }) -const { autoFocus } = toRefs(props) +const { autoFocus, readOnly } = toRefs(props) + +const debouncedLoadMentionFieldTagTooltip = useDebounceFn(loadMentionFieldTagTooltip, 1000) const editor = useEditor({ content: vModel.value, @@ -55,18 +60,25 @@ const editor = useEditor({ ...suggestion(FieldList), items: ({ query }) => { if (query.length === 0) return props.options ?? [] - return props.options?.filter((o) => o.title?.toLowerCase()?.includes(query.toLowerCase())) ?? [] + return ( + props.options?.filter( + (o) => + o.title?.toLowerCase()?.includes(query.toLowerCase()) || `${o.title?.toLowerCase()}}` === query.toLowerCase(), + ) ?? [] + ) }, char: '{', allowSpaces: true, }, renderHTML: ({ node }) => { + const matchedOption = props.options?.find((option) => option.title === node.attrs.id) + const isAttachment = matchedOption?.uidt === UITypes.Attachment return [ 'span', { - class: `prompt-field-tag ${ - props.options?.find((option) => option.title === node.attrs.id)?.uidt === UITypes.Attachment ? '!bg-green-200' : '' - } ${props.promptFieldTagClassName}`, + 'class': `prompt-field-tag ${isAttachment ? '!bg-green-200' : ''} ${props.promptFieldTagClassName}`, + 'style': 'max-width: 100px; white-space: nowrap; overflow: hidden; display: inline-block; text-overflow: ellipsis;', // Enforces truncation + 'data-tooltip': node.attrs.id, // Tooltip content }, `${node.attrs.id}`, ] @@ -93,8 +105,10 @@ const editor = useEditor({ text = text.trim() vModel.value = text + + debouncedLoadMentionFieldTagTooltip() }, - editable: true, + editable: !readOnly.value, autofocus: autoFocus.value, editorProps: { scrollThreshold: 100 }, }) @@ -141,15 +155,60 @@ onMounted(async () => { }, 100) } }) + +const tooltipInstances: any[] = [] + +function loadMentionFieldTagTooltip() { + document.querySelectorAll('.nc-ai-prompt-with-fields .prompt-field-tag').forEach((el) => { + const tooltip = Object.values(el.attributes).find((attr) => attr.name === 'data-tooltip') + if (!tooltip || el.scrollWidth <= el.clientWidth) return + // Show tooltip only on truncate + const instance = tippy(el, { + content: `
${tooltip.value}
`, + placement: 'top', + allowHTML: true, + arrow: true, + animation: 'fade', + duration: 0, + maxWidth: '200px', + }) + + tooltipInstances.push(instance) + }) +} + +onMounted(() => { + debouncedLoadMentionFieldTagTooltip() +}) + +onBeforeUnmount(() => { + tooltipInstances.forEach((instance) => instance?.destroy()) + tooltipInstances.length = 0 +}) - + diff --git a/packages/nc-gui/components/cell/AI.vue b/packages/nc-gui/components/cell/AI.vue index aa5ea1e2c6..ff2e8bbfe5 100644 --- a/packages/nc-gui/components/cell/AI.vue +++ b/packages/nc-gui/components/cell/AI.vue @@ -13,6 +13,10 @@ const { generateRows, generatingRows, generatingColumnRows, aiIntegrations } = u const { row } = useSmartsheetRowStoreOrThrow() +const { isUIAllowed } = useRoles() + +const isPublic = inject(IsPublicInj, ref(false)) + const meta = inject(MetaInj, ref()) const column = inject(ColumnInj) as Ref< @@ -40,9 +44,7 @@ const isAiEdited = ref(false) const isFieldAiIntegrationAvailable = computed(() => { const fkIntegrationId = column.value?.colOptions?.fk_integration_id - if (!fkIntegrationId) return false - - return ncIsArrayIncludes(aiIntegrations.value, fkIntegrationId, 'id') + return !!fkIntegrationId }) const pk = computed(() => { @@ -58,7 +60,7 @@ const generate = async () => { ncIsString(column.value.colOptions?.output_column_ids) && column.value.colOptions.output_column_ids.split(',').length > 1 ? column.value.colOptions.output_column_ids.split(',') : [] - const outputColumns = outputColumnIds.map((id) => meta.value?.columnsById[id]) + const outputColumns = outputColumnIds.map((id) => meta.value?.columnsById?.[id]).filter(Boolean) generatingRows.value.push(pk.value) generatingColumnRows.value.push(column.value.id) @@ -76,11 +78,18 @@ const generate = async () => { } } else { const obj: AIRecordType = resRow[column.value.title!] + if (obj && typeof obj === 'object') { vModel.value = obj setTimeout(() => { isAiEdited.value = false }, 100) + } else { + vModel.value = { + ...(ncIsObject(vModel.value) ? vModel.value : {}), + isStale: false, + value: resRow[column.value.title!], + } } } } @@ -99,10 +108,16 @@ const isLoading = computed(() => { }) const handleSave = () => { + vModel.value = { ...vModel.value } + emits('save') } const debouncedSave = useDebounceFn(handleSave, 1000) + +const isDisabledAiButton = computed(() => { + return !isFieldAiIntegrationAvailable.value || isLoading.value || isPublic.value || !isUIAllowed('dataEdit') +}) -
+
@@ -679,7 +723,7 @@ textarea:focus { diff --git a/packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue b/packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue index 9c8adf6993..cc2dc7d4e0 100644 --- a/packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue +++ b/packages/nc-gui/components/smartsheet/column/UITypesOptionsWithSearch.vue @@ -20,7 +20,7 @@ const filteredOptions = computed( () => options.value?.filter( (c) => - !(c.name === 'AIButton' && !isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)) && + !((c.name === 'AIButton' || c.name === 'AIPrompt') && !isFeatureEnabled(FEATURE_FLAG.AI_FEATURES)) && (c.name.toLowerCase().includes(searchQuery.value.toLowerCase()) || (UITypesName[c.name] && UITypesName[c.name].toLowerCase().includes(searchQuery.value.toLowerCase()))), ) ?? [], @@ -126,7 +126,7 @@ watch( 'hover:bg-gray-100 cursor-pointer': !isDisabledUIType(option.name), 'bg-gray-100 nc-column-list-option-active': activeFieldIndex === index && !isDisabledUIType(option.name), '!text-gray-400 cursor-not-allowed': isDisabledUIType(option.name), - '!text-nc-content-purple-dark': option.name === 'AIButton', + '!text-nc-content-purple-dark': option.name === 'AIButton' || option.name === 'AIPrompt', }, ]" :data-testid="option.name" diff --git a/packages/nc-gui/components/smartsheet/details/Fields.vue b/packages/nc-gui/components/smartsheet/details/Fields.vue index 9dbd53d930..f6dafbe49c 100644 --- a/packages/nc-gui/components/smartsheet/details/Fields.vue +++ b/packages/nc-gui/components/smartsheet/details/Fields.vue @@ -1037,6 +1037,10 @@ const onClickCopyFieldUrl = async (field: ColumnType) => { await copy(field.id!) isFieldIdCopied.value = true + + await ncDelay(5000) + + isFieldIdCopied.value = false } const keys = useMagicKeys() diff --git a/packages/nc-gui/components/smartsheet/expanded-form/index.vue b/packages/nc-gui/components/smartsheet/expanded-form/index.vue index dec44ec27c..f92d9fb778 100644 --- a/packages/nc-gui/components/smartsheet/expanded-form/index.vue +++ b/packages/nc-gui/components/smartsheet/expanded-form/index.vue @@ -327,6 +327,10 @@ const copyRecordUrl = async () => { ) isRecordLinkCopied.value = true + + await ncDelay(5000) + + isRecordLinkCopied.value = false } const saveChanges = async () => { diff --git a/packages/nc-gui/components/smartsheet/grid/GroupByTable.vue b/packages/nc-gui/components/smartsheet/grid/GroupByTable.vue index a8cd49ace6..a593c281c2 100644 --- a/packages/nc-gui/components/smartsheet/grid/GroupByTable.vue +++ b/packages/nc-gui/components/smartsheet/grid/GroupByTable.vue @@ -276,6 +276,7 @@ async function deleteSelectedRowsWrapper() {