多维表格
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

579 lines
17 KiB

import { UITypes } from 'nocodb-sdk'
import type { WritableComputedRef } from '@vue/reactivity'
import type { RuleObject } from 'ant-design-vue/es/form'
import { AiWizardTabsType, type PredictedFieldType } from '#imports'
enum AiStep {
init = 'init',
pick = 'pick',
}
const maxSelectionCount = 100
const useForm = Form.useForm
export const usePredictFields = createSharedComposable(
(isFromTableExplorer?: Ref<boolean>, fields?: WritableComputedRef<Record<string, any>[]>) => {
const { t } = useI18n()
const { aiLoading, aiError, predictNextFields: _predictNextFields, predictNextFormulas } = useNocoAi()
const { meta, view } = useSmartsheetStoreOrThrow()
const isForm = inject(IsFormInj, ref(false))
const columnsHash = ref<string>()
const { $api } = useNuxtApp()
const aiMode = ref(false)
const localIsFromFieldModal = ref<boolean>(false)
const isAiModeFieldModal = computed(() => {
return aiMode.value && localIsFromFieldModal.value
})
const isFormulaPredictionMode = ref(false)
const aiModeStep = ref<AiStep | null>(null)
const calledFunction = ref<string>('')
const prompt = ref<string>('')
const oldPrompt = ref<string>('')
const isPromtAlreadyGenerated = ref<boolean>(false)
const activeAiTabLocal = ref<AiWizardTabsType>(AiWizardTabsType.AUTO_SUGGESTIONS)
const failedToSaveFields = ref<boolean>(false)
const temporaryAddCount = ref<number>(0)
const activeAiTab = computed({
get: () => {
return activeAiTabLocal.value
},
set: (value: AiWizardTabsType) => {
activeAiTabLocal.value = value
aiError.value = ''
},
})
const predicted = ref<PredictedFieldType[]>([])
const activeTabPredictedFields = computed(() => predicted.value.filter((f) => f.tab === activeAiTab.value))
const removedFromPredicted = ref<PredictedFieldType[]>([])
const predictHistory = ref<PredictedFieldType[]>([])
const activeTabPredictHistory = computed(() => predictHistory.value.filter((f) => f.tab === activeAiTab.value))
const activeSelectedField = ref<string | null>(null)
const selected = ref<PredictedFieldType[]>([])
const activeTabSelectedFields = computed(() => {
return predicted.value.filter((field) => !!field.selected && field.tab === activeAiTab.value)
})
const isPredictFromPromptLoading = computed(() => {
return aiLoading.value && calledFunction.value === 'predictFromPrompt'
})
const validators = computed(() => {
const rulesObj: Record<string, RuleObject[]> = {}
if (!activeTabSelectedFields.value.length) return rulesObj
for (const column of activeTabSelectedFields.value) {
const rules: RuleObject[] = [
{
validator: (_rule: RuleObject, value: any) => {
return new Promise((resolve, reject) => {
const isAiFieldExist = isAiModeFieldModal.value
? activeTabSelectedFields.value.some((c) => {
return (
c.ai_temp_id !== value?.ai_temp_id &&
value?.title &&
(value.title.toLowerCase().trim() === (c.formState?.column_name || '').toLowerCase().trim() ||
value.title.toLowerCase().trim() === (c.formState?.title || '').toLowerCase().trim() ||
value.title.toLowerCase().trim() === (c?.title || '').toLowerCase().trim())
)
})
: false
if (isAiFieldExist) {
return reject(new Error(`${t('msg.error.duplicateColumnName')} "${value?.title}"`))
}
return resolve()
})
},
},
]
switch (column.type) {
case UITypes.Formula: {
rules.push({
validator: (_rule: RuleObject, value: any) => {
return new Promise((resolve, reject) => {
if (!value?.formula_raw?.trim()) {
return reject(new Error('Formula is required'))
}
return resolve()
})
},
})
}
}
if (rules.length) {
rulesObj[column.ai_temp_id] = rules
}
}
return rulesObj
})
const fieldMappingFormState = computed(() => {
if (!activeTabSelectedFields.value.length) return {}
return activeTabSelectedFields.value.reduce((acc, col) => {
acc[col.ai_temp_id] = col.formState
return acc
}, {} as Record<string, any>)
})
// Form field validation
const { validate } = useForm(fieldMappingFormState, validators)
const getFieldWithDefautlValue = (field: PredictedFieldType) => {
if ([UITypes.SingleSelect, UITypes.MultiSelect].includes(field.type)) {
if (field.options) {
const options: {
title: string
index: number
color?: string
}[] = []
for (const option of field.options) {
// skip if option already exists
if (options.find((el) => el.title === option)) continue
options.push({
title: option,
index: options.length,
color: enumColor.light[options.length % enumColor.light.length],
})
}
field.colOptions = {
options,
}
}
}
return {
title: field.title,
uidt: isFormulaPredictionMode.value ? UITypes.Formula : field.type,
column_name: field.title.toLowerCase().replace(/\\W/g, '_'),
...(field.formula ? { formula_raw: field.formula } : {}),
...(field.colOptions ? { colOptions: field.colOptions } : {}),
meta: {
...(field.type in columnDefaultMeta ? columnDefaultMeta[field.type as keyof typeof columnDefaultMeta] : {}),
},
description: field?.description || null,
is_ai_field: true,
ai_temp_id: field.ai_temp_id,
}
}
const predictNextFields = async (): Promise<PredictedFieldType[]> => {
const fieldHistory = Array.from(
new Set(
activeTabPredictHistory.value
.map((f) => f.title)
.concat(
isFromTableExplorer?.value
? fields?.value?.filter((f) => !!f?.title && !!f?.temp_id)?.map((f) => f.title) || []
: [],
),
),
)
return (
await (isFormulaPredictionMode.value
? predictNextFormulas(
meta.value?.id as string,
fieldHistory,
meta.value?.base_id,
activeAiTab.value === AiWizardTabsType.PROMPT ? prompt.value : undefined,
)
: _predictNextFields(
meta.value?.id as string,
fieldHistory,
meta.value?.base_id,
activeAiTab.value === AiWizardTabsType.PROMPT ? prompt.value : undefined,
isForm.value ? formViewHiddenColTypes : [],
))
)
.filter(
(f) =>
!ncIsArrayIncludes(
[
...activeTabPredictedFields.value,
...(isFromTableExplorer?.value
? fields?.value?.filter((f) => !!f?.title && !!f?.temp_id).map((f) => ({ title: f.title })) || []
: []),
],
f.title,
'title',
),
)
.map((f) => {
const state = {
...f,
tab: activeAiTab.value,
ai_temp_id: `temp_${++temporaryAddCount.value}`,
selected: false,
}
if (isFromTableExplorer?.value) {
return state
}
return {
...state,
formState: getFieldWithDefautlValue(state),
}
})
}
const disableAiMode = () => {
onInit()
}
const predictMore = async () => {
calledFunction.value = 'predictMore'
const predictions = await predictNextFields()
if (predictions.length) {
predicted.value.push(...predictions)
predictHistory.value.push(...predictions)
} else if (!aiError.value) {
message.info(`No more auto suggestions were found for ${meta.value?.title || 'the current table'}`)
}
}
const predictRefresh = async (callback?: (field?: PredictedFieldType | undefined) => void) => {
calledFunction.value = 'predictRefresh'
const predictions = await predictNextFields()
if (predictions.length) {
predicted.value = [
...predicted.value.filter(
(t) => t.tab !== activeAiTab.value || (isFromTableExplorer?.value && t.tab === activeAiTab.value && !!t.selected),
),
...predictions,
]
predictHistory.value.push(...predictions)
if (ncIsFunction(callback)) {
callback()
}
} else if (!aiError.value) {
message.info(`No auto suggestions were found for ${meta.value?.title || 'the current table'}`)
}
aiModeStep.value = AiStep.pick
}
const predictFromPrompt = async (callback?: (field?: PredictedFieldType | undefined) => void) => {
calledFunction.value = 'predictFromPrompt'
const predictions = await predictNextFields()
if (predictions.length) {
predicted.value = [
...predicted.value.filter(
(t) => t.tab !== activeAiTab.value || (isFromTableExplorer?.value && t.tab === activeAiTab.value && !!t.selected),
),
...predictions,
]
predictHistory.value.push(...predictions)
oldPrompt.value = prompt.value
if (ncIsFunction(callback)) {
callback()
}
} else if (!aiError.value) {
message.info('No suggestions were found with the given prompt. Try again after modifying the prompt.')
}
aiModeStep.value = AiStep.pick
isPromtAlreadyGenerated.value = true
}
// Todo: update logic
const onToggleTag = (field: PredictedFieldType) => {
if (
field.selected !== true &&
(activeTabSelectedFields.value.length >= maxSelectionCount ||
ncIsArrayIncludes(
predicted.value.filter((f) => !!f.selected),
field.title,
'title',
))
) {
return
}
if (isFromTableExplorer?.value && field.selected) {
const fieldIndex = predicted.value.findIndex((f) => f.ai_temp_id === field.ai_temp_id)
if (fieldIndex === -1) return
const fieldToDeselect = predicted.value.splice(fieldIndex, 1)[0]
fieldToDeselect.selected = false
predicted.value.push(fieldToDeselect)
} else {
predicted.value = predicted.value.map((t) => {
if (t.ai_temp_id === field.ai_temp_id) {
if (!isFromTableExplorer?.value && !field.selected) {
activeSelectedField.value = field.ai_temp_id
}
t.selected = !field.selected
}
return t
})
}
return true
}
const onSelectedTagClick = (field: PredictedFieldType) => {
activeSelectedField.value = field.ai_temp_id
}
const onSelectAll = () => {
if (selected.value.length >= maxSelectionCount) return
let count = selected.value.length
const remainingPredictedFields: PredictedFieldType[] = []
const fieldsToAdd: PredictedFieldType[] = []
predicted.value.forEach((pv) => {
// Check if the item can be selected
if (
count < maxSelectionCount &&
!ncIsArrayIncludes(removedFromPredicted.value, pv.title, 'title') &&
!ncIsArrayIncludes(selected.value, pv.title, 'title')
) {
fieldsToAdd.push(pv) // Add to selected field if it meets the criteria
count++
} else {
remainingPredictedFields.push(pv) // Keep in predicted field if it doesn't meet the criteria
}
})
// Add selected items to the selected view array
selected.value.push(
...fieldsToAdd.map((f) => {
if (!isFromTableExplorer?.value) {
f.formState = getFieldWithDefautlValue(f)
}
return f
}),
)
// Update predicted with the remaining ones
predicted.value = remainingPredictedFields
return fieldsToAdd
}
const handleRefreshOnError = () => {
switch (calledFunction.value) {
case 'predictMore':
return predictMore()
case 'predictRefresh':
return predictRefresh()
case 'predictFromPrompt':
return predictFromPrompt()
default:
}
}
const validateAllFields = async () => {
try {
await validate()
return true
} catch (e: any) {
console.error(e)
if (e?.errorFields?.length) {
const errorMsg = e?.errorFields
.map((field, idx) => (field?.errors?.length ? `${idx + 1}. ${field?.errors?.join(',')}` : ''))
.join(', ')
message.error(errorMsg || t('msg.error.someOfTheRequiredFieldsAreEmpty'))
return false
}
}
}
const saveFields = async (onSuccess: () => Promise<void>) => {
const isValid = await validateAllFields()
if (!isValid) return false
failedToSaveFields.value = false
const payload = activeTabSelectedFields.value
.filter((f) => f.formState)
.map((field) => {
return {
op: 'add',
column: {
...field.formState,
title: field.formState?.title || field.title,
column_name: (field.formState?.title || field.title).toLowerCase().replace(/\\W/g, '_'),
column_order: {
// order: order,
view_id: view.value?.id,
},
view_id: view.value?.id,
},
}
})
try {
const res = await $api.dbTableColumn.bulk(meta.value?.id, {
hash: columnsHash.value,
ops: payload,
})
if (res && res.failedOps?.length) {
const failedColumnTitle = res.failedOps.filter((o) => o?.column?.ai_temp_id).map((o) => o.column.ai_temp_id)
predicted.value = predicted.value.filter((f) => {
if (failedColumnTitle.includes(f.formState?.ai_temp_id)) return true
return false
})
failedToSaveFields.value = true
return false
} else {
await onSuccess?.()
return true
}
} catch (e: any) {
console.error(e)
return false
}
}
const toggleAiMode = async (isFormulaMode: boolean = false, fromFieldModal = false) => {
if (isFormulaMode) {
isFormulaPredictionMode.value = true
} else {
isFormulaPredictionMode.value = false
}
localIsFromFieldModal.value = !!fromFieldModal
aiError.value = ''
aiMode.value = true
aiModeStep.value = AiStep.init
predicted.value = []
predictHistory.value = []
prompt.value = ''
oldPrompt.value = ''
isPromtAlreadyGenerated.value = false
const predictions = await predictNextFields()
predicted.value = predictions
predictHistory.value.push(...predictions)
aiModeStep.value = AiStep.pick
}
function onInit() {
activeSelectedField.value = null
isFormulaPredictionMode.value = false
aiMode.value = false
localIsFromFieldModal.value = false
aiModeStep.value = null
predicted.value = []
removedFromPredicted.value = []
predictHistory.value = []
selected.value = []
calledFunction.value = ''
prompt.value = ''
oldPrompt.value = ''
isPromtAlreadyGenerated.value = false
activeAiTabLocal.value = AiWizardTabsType.AUTO_SUGGESTIONS
failedToSaveFields.value = false
}
watch(
meta,
async (newMeta) => {
if (newMeta?.id) {
columnsHash.value = (await $api.dbTableColumn.hash(newMeta.id)).hash
predictHistory.value = []
}
},
{ deep: true, immediate: true },
)
return {
aiMode,
isAiModeFieldModal,
aiModeStep,
predicted,
activeTabPredictedFields,
removedFromPredicted,
predictHistory,
activeTabPredictHistory,
activeSelectedField,
selected,
activeTabSelectedFields,
calledFunction,
prompt,
oldPrompt,
isPromtAlreadyGenerated,
maxSelectionCount,
activeAiTab,
isPredictFromPromptLoading,
isFormulaPredictionMode,
failedToSaveFields,
onInit,
toggleAiMode,
disableAiMode,
predictMore,
predictRefresh,
predictFromPrompt,
onToggleTag,
onSelectedTagClick,
onSelectAll,
handleRefreshOnError,
saveFields,
}
},
)