@swiss-ai-hub/web 0.320.0 → 0.321.1
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/components/FormKit/AgentSelector.vue +20 -7
- package/components/FormKit/Repeater.vue +1 -4
- package/components/FormKit/VectorStoreInput.vue +43 -6
- package/components/Knowledge/Database/CreateModal.vue +315 -0
- package/components/Knowledge/Database/EmptyCard.vue +34 -0
- package/components/Knowledge/DeleteConfirmModal.vue +87 -0
- package/components/Knowledge/Namespace/Card.vue +18 -0
- package/components/Knowledge/Namespace/CreateModal.vue +11 -3
- package/components/Role/AccessCapabilities.vue +4 -4
- package/components/Role/AccessCapabilityGroup.vue +22 -6
- package/components/Role/AccessRulesEditor.vue +15 -12
- package/composables/app/useAppVersion.ts +31 -33
- package/composables/auth/useAuth.ts +3 -11
- package/composables/document/useCreateDatabase.ts +21 -0
- package/composables/document/useDeleteDatabase.ts +29 -0
- package/composables/document/useDeleteDocument.ts +1 -1
- package/composables/document/useDeleteDocuments.ts +1 -1
- package/composables/document/useDeleteNamespace.ts +30 -0
- package/composables/document/useIngestors.ts +23 -0
- package/composables/form/useCreateInstanceForm.ts +2 -2
- package/composables/form/useFormKitTransform.ts +44 -15
- package/formkit.config.ts +24 -1
- package/i18n/locales/de.yaml +60 -5
- package/i18n/locales/en.yaml +57 -3
- package/i18n/locales/fr.yaml +58 -3
- package/i18n/locales/it.yaml +60 -3
- package/middleware/agent-admin.ts +31 -0
- package/package.json +1 -1
- package/pages/[tenant]/service/agents.vue +3 -0
- package/pages/[tenant]/service/knowledge/[db]/[namespace]/[document_id].vue +12 -5
- package/pages/[tenant]/service/knowledge.vue +138 -0
- package/plugins/0.runtime-config.client.ts +5 -0
- package/plugins/oidc-client.ts +1 -1
- package/sdk/client/index.ts +29 -2
- package/sdk/client/schemas.gen.ts +641 -77
- package/sdk/client/sdk.gen.ts +159 -1
- package/sdk/client/types.gen.ts +543 -75
- package/plugins/keycloak-client.ts +0 -41
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="flex flex-col gap-4">
|
|
3
|
-
<!-- Agent Class Selection -->
|
|
4
|
-
<div>
|
|
3
|
+
<!-- Agent Class Selection (omitted when the config pins the class) -->
|
|
4
|
+
<div v-if="!pinnedClass">
|
|
5
5
|
<label
|
|
6
6
|
for="agent-class-select"
|
|
7
7
|
class="mb-1 block text-sm font-medium"
|
|
@@ -139,6 +139,7 @@ interface AgentSelectorProps {
|
|
|
139
139
|
value?: AgentSelectorValue | null
|
|
140
140
|
attrs: Record<string, unknown>
|
|
141
141
|
startEvent?: string
|
|
142
|
+
agentClass?: string
|
|
142
143
|
classPlaceholder?: string
|
|
143
144
|
idPlaceholder?: string
|
|
144
145
|
filter?: boolean
|
|
@@ -151,6 +152,9 @@ const { tenantId } = useTenant()
|
|
|
151
152
|
|
|
152
153
|
// Get custom props from context (FormKit passes them there, not as direct props)
|
|
153
154
|
const startEvent = computed(() => props.context.startEvent)
|
|
155
|
+
// Set by configs that already know which blueprint answers: the class dropdown is not rendered and only
|
|
156
|
+
// this class's profiles are offered.
|
|
157
|
+
const pinnedClass = computed(() => props.context.agentClass)
|
|
154
158
|
const classPlaceholder = computed(() => props.context.classPlaceholder)
|
|
155
159
|
const idPlaceholder = computed(() => props.context.idPlaceholder)
|
|
156
160
|
const filter = computed(() => props.context.filter ?? true)
|
|
@@ -166,7 +170,7 @@ const currentValue = computed(() => props.context.value ?? null)
|
|
|
166
170
|
|
|
167
171
|
// Selected class (computed property that syncs with the form value)
|
|
168
172
|
const selectedClass = computed({
|
|
169
|
-
get: () => currentValue.value?.agent_class ?? null,
|
|
173
|
+
get: () => currentValue.value?.agent_class ?? pinnedClass.value ?? null,
|
|
170
174
|
set: (value: string | null) => {
|
|
171
175
|
if (value) {
|
|
172
176
|
// When class changes, reset agent ID and fetch instances
|
|
@@ -290,16 +294,25 @@ async function fetchInstances(agentClass: string) {
|
|
|
290
294
|
|
|
291
295
|
// Fetch classes on mount
|
|
292
296
|
onMounted(() => {
|
|
293
|
-
|
|
297
|
+
// A pinned class needs no class list — nothing renders it, and the profile dropdown is driven by
|
|
298
|
+
// fetchInstances alone.
|
|
299
|
+
if (!pinnedClass.value) {
|
|
300
|
+
fetchClasses()
|
|
301
|
+
}
|
|
294
302
|
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
303
|
+
// Pinned, or already chosen: either way the profile dropdown needs that class's instances up front.
|
|
304
|
+
const classToLoad = currentValue.value?.agent_class ?? pinnedClass.value
|
|
305
|
+
if (classToLoad) {
|
|
306
|
+
fetchInstances(classToLoad)
|
|
298
307
|
}
|
|
299
308
|
})
|
|
300
309
|
|
|
301
310
|
// Refetch if startEvent changes
|
|
302
311
|
watch(startEvent, () => {
|
|
312
|
+
// A pinned class is not filtered by start event, and its class list is never fetched — so the check below
|
|
313
|
+
// would find no match and clear a perfectly valid selection.
|
|
314
|
+
if (pinnedClass.value) return
|
|
315
|
+
|
|
303
316
|
// Clear selection if current class no longer matches filter
|
|
304
317
|
if (selectedClass.value && !filteredClassOptions.value.some(opt => opt.name === selectedClass.value)) {
|
|
305
318
|
props.context.node.input(null)
|
|
@@ -99,11 +99,8 @@ const isRemoveDisabled = computed(() => {
|
|
|
99
99
|
|
|
100
100
|
function addItem() {
|
|
101
101
|
if (isAddDisabled.value) return
|
|
102
|
-
if (!modelValue.value) {
|
|
103
|
-
modelValue.value = []
|
|
104
|
-
}
|
|
105
102
|
rowKeys.value.push(makeRowKey())
|
|
106
|
-
modelValue.value.
|
|
103
|
+
modelValue.value = [...items.value, props.defaultItem ? cloneDeep(props.defaultItem) : {}]
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
function removeItem(index: number) {
|
|
@@ -44,8 +44,28 @@
|
|
|
44
44
|
</Select>
|
|
45
45
|
</div>
|
|
46
46
|
|
|
47
|
-
<!-- Namespace
|
|
48
|
-
<div
|
|
47
|
+
<!-- Namespace scope (shown when database selected) -->
|
|
48
|
+
<div
|
|
49
|
+
v-if="selectedDatabase"
|
|
50
|
+
class="flex items-center gap-2"
|
|
51
|
+
>
|
|
52
|
+
<ToggleSwitch
|
|
53
|
+
v-model="allNamespaces"
|
|
54
|
+
input-id="vector-store-all-namespaces"
|
|
55
|
+
/>
|
|
56
|
+
<label
|
|
57
|
+
for="vector-store-all-namespaces"
|
|
58
|
+
class="text-sm"
|
|
59
|
+
>{{ t('lib.vectorStore.allNamespaces.label') }}</label>
|
|
60
|
+
</div>
|
|
61
|
+
<small
|
|
62
|
+
v-if="selectedDatabase && allNamespaces"
|
|
63
|
+
class="-mt-3 text-surface-500"
|
|
64
|
+
>
|
|
65
|
+
{{ t('lib.vectorStore.allNamespaces.help', { count: namespaceOptions.length }) }}
|
|
66
|
+
</small>
|
|
67
|
+
|
|
68
|
+
<div v-if="selectedDatabase && !allNamespaces">
|
|
49
69
|
<label
|
|
50
70
|
for="vector-store-namespaces-select"
|
|
51
71
|
class="mb-1 block text-sm font-medium"
|
|
@@ -104,6 +124,7 @@ import type { DatabaseDto } from '@core/sdk/client'
|
|
|
104
124
|
interface VectorStoreInputValue {
|
|
105
125
|
collection_name: string
|
|
106
126
|
index_namespaces: string[]
|
|
127
|
+
all_namespaces: boolean
|
|
107
128
|
allowed_metadata_filter_fields: string[]
|
|
108
129
|
}
|
|
109
130
|
|
|
@@ -154,7 +175,7 @@ const selectedDatabase = computed({
|
|
|
154
175
|
set: (value: string | null) => {
|
|
155
176
|
if (value) {
|
|
156
177
|
// When database changes, reset namespaces and allowed filter fields
|
|
157
|
-
emitValue(value, [], [])
|
|
178
|
+
emitValue(value, [], false, [])
|
|
158
179
|
}
|
|
159
180
|
else {
|
|
160
181
|
props.context.node.input(null)
|
|
@@ -162,12 +183,22 @@ const selectedDatabase = computed({
|
|
|
162
183
|
},
|
|
163
184
|
})
|
|
164
185
|
|
|
186
|
+
// Whole-database scope: switching it on discards any named namespaces so the two never coexist
|
|
187
|
+
const allNamespaces = computed({
|
|
188
|
+
get: () => currentValue.value?.all_namespaces ?? false,
|
|
189
|
+
set: (value: boolean) => {
|
|
190
|
+
if (selectedDatabase.value) {
|
|
191
|
+
emitValue(selectedDatabase.value, value ? [] : selectedNamespaces.value, value, allowedFilterFields.value)
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
|
|
165
196
|
// Selected namespaces (computed property that syncs with the form value)
|
|
166
197
|
const selectedNamespaces = computed({
|
|
167
198
|
get: () => currentValue.value?.index_namespaces ?? [],
|
|
168
199
|
set: (value: string[]) => {
|
|
169
200
|
if (selectedDatabase.value) {
|
|
170
|
-
emitValue(selectedDatabase.value, value, allowedFilterFields.value)
|
|
201
|
+
emitValue(selectedDatabase.value, value, false, allowedFilterFields.value)
|
|
171
202
|
}
|
|
172
203
|
},
|
|
173
204
|
})
|
|
@@ -179,7 +210,7 @@ const chipsInputContext = computed(() => ({
|
|
|
179
210
|
node: {
|
|
180
211
|
input: (value: string[]) => {
|
|
181
212
|
if (selectedDatabase.value) {
|
|
182
|
-
emitValue(selectedDatabase.value, selectedNamespaces.value, value)
|
|
213
|
+
emitValue(selectedDatabase.value, selectedNamespaces.value, allNamespaces.value, value)
|
|
183
214
|
}
|
|
184
215
|
},
|
|
185
216
|
},
|
|
@@ -189,10 +220,16 @@ const chipsInputContext = computed(() => ({
|
|
|
189
220
|
}))
|
|
190
221
|
|
|
191
222
|
// Emit complete value object
|
|
192
|
-
function emitValue(
|
|
223
|
+
function emitValue(
|
|
224
|
+
collectionName: string,
|
|
225
|
+
namespaces: string[],
|
|
226
|
+
wholeDatabase: boolean,
|
|
227
|
+
allowedFilterFieldKeys: string[],
|
|
228
|
+
) {
|
|
193
229
|
props.context.node.input({
|
|
194
230
|
collection_name: collectionName,
|
|
195
231
|
index_namespaces: namespaces,
|
|
232
|
+
all_namespaces: wholeDatabase,
|
|
196
233
|
allowed_metadata_filter_fields: allowedFilterFieldKeys,
|
|
197
234
|
})
|
|
198
235
|
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Dialog
|
|
3
|
+
v-model:visible="visible"
|
|
4
|
+
modal
|
|
5
|
+
:header="t('knowledge.form.create_database.title')"
|
|
6
|
+
:style="{ width: '50rem' }"
|
|
7
|
+
:closable="!isSubmitting"
|
|
8
|
+
>
|
|
9
|
+
<div class="flex flex-col gap-6">
|
|
10
|
+
<div
|
|
11
|
+
v-if="ingestorsAreLoading"
|
|
12
|
+
class="flex items-center justify-center py-8"
|
|
13
|
+
>
|
|
14
|
+
<ProgressSpinner />
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
v-else-if="!ingestors || ingestors.length === 0"
|
|
19
|
+
class="py-8 text-center text-surface-500"
|
|
20
|
+
>
|
|
21
|
+
{{ t('knowledge.form.create_database.no_ingestors') }}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<template v-else>
|
|
25
|
+
<div class="flex flex-col gap-2">
|
|
26
|
+
<label
|
|
27
|
+
for="database-name-input"
|
|
28
|
+
class="text-sm font-medium"
|
|
29
|
+
>
|
|
30
|
+
{{ t('knowledge.form.database_name.label') }}
|
|
31
|
+
<span class="ml-1 text-xs text-red-500">*</span>
|
|
32
|
+
</label>
|
|
33
|
+
<InputText
|
|
34
|
+
id="database-name-input"
|
|
35
|
+
v-model="databaseName"
|
|
36
|
+
:placeholder="t('knowledge.form.database_name.placeholder')"
|
|
37
|
+
:class="{ 'p-invalid': nameValidationError }"
|
|
38
|
+
:disabled="isSubmitting"
|
|
39
|
+
/>
|
|
40
|
+
<small
|
|
41
|
+
v-if="nameValidationError"
|
|
42
|
+
class="text-red-500"
|
|
43
|
+
>{{ nameValidationError }}</small>
|
|
44
|
+
<small
|
|
45
|
+
v-else
|
|
46
|
+
class="text-gray-500"
|
|
47
|
+
>{{ t('knowledge.form.database_name.help') }}</small>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="flex flex-col gap-2">
|
|
51
|
+
<label
|
|
52
|
+
for="database-ingestor-select"
|
|
53
|
+
class="text-sm font-medium"
|
|
54
|
+
>
|
|
55
|
+
{{ t('knowledge.form.ingestor.label') }}
|
|
56
|
+
<span class="ml-1 text-xs text-red-500">*</span>
|
|
57
|
+
</label>
|
|
58
|
+
<Select
|
|
59
|
+
id="database-ingestor-select"
|
|
60
|
+
v-model="selectedClass"
|
|
61
|
+
:options="ingestors"
|
|
62
|
+
option-label="display_name"
|
|
63
|
+
option-value="name"
|
|
64
|
+
:placeholder="t('knowledge.form.ingestor.placeholder')"
|
|
65
|
+
:disabled="isSubmitting"
|
|
66
|
+
class="w-full"
|
|
67
|
+
/>
|
|
68
|
+
<small class="text-gray-500">
|
|
69
|
+
{{ selectedClassData?.description || t('knowledge.form.ingestor.help') }}
|
|
70
|
+
</small>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div
|
|
74
|
+
v-if="selectedClassData && configForm.length > 0 && formReady"
|
|
75
|
+
class="content flex flex-col gap-2"
|
|
76
|
+
>
|
|
77
|
+
<FormKit
|
|
78
|
+
id="create-database-form"
|
|
79
|
+
v-model="formData"
|
|
80
|
+
type="form"
|
|
81
|
+
:actions="false"
|
|
82
|
+
:config="{
|
|
83
|
+
validationVisibility: 'dirty',
|
|
84
|
+
}"
|
|
85
|
+
@submit="handleFormSubmit"
|
|
86
|
+
@submit-invalid="isSubmitting = false"
|
|
87
|
+
>
|
|
88
|
+
<Stepper
|
|
89
|
+
v-model:value="activeStep"
|
|
90
|
+
orientation="vertical"
|
|
91
|
+
>
|
|
92
|
+
<StepItem
|
|
93
|
+
v-if="simpleElementsSchema.length > 0"
|
|
94
|
+
:value="0"
|
|
95
|
+
>
|
|
96
|
+
<Step>{{ t('knowledge.form.create_database.steps.basic_info') }}</Step>
|
|
97
|
+
<StepPanel>
|
|
98
|
+
<div class="flex flex-col gap-6 py-4">
|
|
99
|
+
<FormKitSchema
|
|
100
|
+
:schema="simpleElementsSchema"
|
|
101
|
+
:data="formData"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</StepPanel>
|
|
105
|
+
</StepItem>
|
|
106
|
+
<StepItem
|
|
107
|
+
v-for="(group, index) in groupConfigs"
|
|
108
|
+
:key="`group-${group.name}`"
|
|
109
|
+
:value="getGroupStepIndex(index)"
|
|
110
|
+
>
|
|
111
|
+
<Step>{{ group.label || group.name }}</Step>
|
|
112
|
+
<StepPanel>
|
|
113
|
+
<div class="content py-4">
|
|
114
|
+
<FormKitSchema
|
|
115
|
+
:schema="group.schema"
|
|
116
|
+
:data="formData"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
</StepPanel>
|
|
120
|
+
</StepItem>
|
|
121
|
+
<StepItem
|
|
122
|
+
v-for="(rep, index) in repeaterConfigs"
|
|
123
|
+
:key="`repeater-${rep.path}`"
|
|
124
|
+
:value="getRepeaterStepIndex(index)"
|
|
125
|
+
>
|
|
126
|
+
<Step>{{ rep.label || rep.name }}</Step>
|
|
127
|
+
<StepPanel>
|
|
128
|
+
<div class="py-4">
|
|
129
|
+
<FormKitRepeater
|
|
130
|
+
:model-value="getRepeaterData(rep.path)"
|
|
131
|
+
:name="rep.name"
|
|
132
|
+
:label="rep.label"
|
|
133
|
+
:add-label="rep.addLabel"
|
|
134
|
+
:children-schema="rep.childrenSchema"
|
|
135
|
+
:default-item="rep.defaultItem"
|
|
136
|
+
:min="rep.min"
|
|
137
|
+
:max="rep.max"
|
|
138
|
+
@update:model-value="setRepeaterData(rep.path, $event)"
|
|
139
|
+
/>
|
|
140
|
+
</div>
|
|
141
|
+
</StepPanel>
|
|
142
|
+
</StepItem>
|
|
143
|
+
</Stepper>
|
|
144
|
+
</FormKit>
|
|
145
|
+
</div>
|
|
146
|
+
</template>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<template #footer>
|
|
150
|
+
<Button
|
|
151
|
+
:label="t('knowledge.actions.cancel')"
|
|
152
|
+
severity="secondary"
|
|
153
|
+
outlined
|
|
154
|
+
@click="closeModal"
|
|
155
|
+
/>
|
|
156
|
+
<Button
|
|
157
|
+
:label="t('knowledge.actions.create')"
|
|
158
|
+
:disabled="!canSubmit"
|
|
159
|
+
:loading="isSubmitting"
|
|
160
|
+
@click="triggerFormSubmit"
|
|
161
|
+
/>
|
|
162
|
+
</template>
|
|
163
|
+
</Dialog>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<script setup lang="ts">
|
|
167
|
+
import { type FormElement, serializeFormData } from '@core/composables/form/useFormKitTransform'
|
|
168
|
+
import { getNode } from '@formkit/core'
|
|
169
|
+
|
|
170
|
+
const props = defineProps<{
|
|
171
|
+
modelValue: boolean
|
|
172
|
+
}>()
|
|
173
|
+
|
|
174
|
+
const emit = defineEmits<{
|
|
175
|
+
'update:modelValue': [value: boolean]
|
|
176
|
+
'success': [data: { database: string }]
|
|
177
|
+
}>()
|
|
178
|
+
|
|
179
|
+
const { t, locale } = useI18n()
|
|
180
|
+
const toast = useToast()
|
|
181
|
+
const { mutateAsync: createDatabase } = useCreateDatabase()
|
|
182
|
+
const { ingestors, ingestorsAreLoading } = useIngestors()
|
|
183
|
+
const { tenantId } = useTenant()
|
|
184
|
+
|
|
185
|
+
// The ingestor list is the "class" list: each ingestor announces the form its databases are configured
|
|
186
|
+
// through, exactly as an agent class does, so the agent create flow is reused as is.
|
|
187
|
+
const {
|
|
188
|
+
selectedClass,
|
|
189
|
+
formData,
|
|
190
|
+
activeStep,
|
|
191
|
+
selectedClassData,
|
|
192
|
+
configForm,
|
|
193
|
+
simpleElementsSchema,
|
|
194
|
+
groupConfigs,
|
|
195
|
+
repeaterConfigs,
|
|
196
|
+
getGroupStepIndex,
|
|
197
|
+
getRepeaterStepIndex,
|
|
198
|
+
getRepeaterData,
|
|
199
|
+
setRepeaterData,
|
|
200
|
+
resetForm,
|
|
201
|
+
} = useCreateInstanceForm({
|
|
202
|
+
classes: ingestors,
|
|
203
|
+
classField: 'name',
|
|
204
|
+
idField: 'name',
|
|
205
|
+
initialClass: () => ingestors.value?.[0]?.name ?? '',
|
|
206
|
+
locale,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
const databaseName = ref('')
|
|
210
|
+
const isSubmitting = ref(false)
|
|
211
|
+
const formReady = ref(false)
|
|
212
|
+
|
|
213
|
+
const visible = computed({
|
|
214
|
+
get: () => props.modelValue,
|
|
215
|
+
set: (value: boolean) => emit('update:modelValue', value),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
watch(visible, async (isVisible) => {
|
|
219
|
+
if (!isVisible) {
|
|
220
|
+
formReady.value = false
|
|
221
|
+
databaseName.value = ''
|
|
222
|
+
resetForm()
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
await nextTick()
|
|
226
|
+
formReady.value = true
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const nameValidationError = computed(() => {
|
|
230
|
+
if (!databaseName.value.trim()) return ''
|
|
231
|
+
|
|
232
|
+
const namePattern = /^[a-z][a-z0-9]{2,62}$/
|
|
233
|
+
if (!namePattern.test(databaseName.value)) {
|
|
234
|
+
return t('knowledge.form.database_name.validation_error')
|
|
235
|
+
}
|
|
236
|
+
return ''
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const canSubmit = computed(
|
|
240
|
+
() => databaseName.value.trim() && selectedClass.value && !nameValidationError.value && !isSubmitting.value,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
function closeModal() {
|
|
244
|
+
visible.value = false
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function triggerFormSubmit() {
|
|
248
|
+
if (!canSubmit.value) return
|
|
249
|
+
isSubmitting.value = true
|
|
250
|
+
const formNode = getNode('create-database-form')
|
|
251
|
+
if (formNode) {
|
|
252
|
+
formNode.submit()
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
handleFormSubmit()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function handleFormSubmit() {
|
|
260
|
+
const database = databaseName.value
|
|
261
|
+
try {
|
|
262
|
+
await createDatabase({
|
|
263
|
+
database,
|
|
264
|
+
tenantId: tenantId.value!,
|
|
265
|
+
request: {
|
|
266
|
+
ingestor: selectedClass.value,
|
|
267
|
+
configuration: serializeFormData(formData.value, configForm.value as FormElement[]),
|
|
268
|
+
},
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
toast.add({
|
|
272
|
+
severity: 'success',
|
|
273
|
+
summary: t('knowledge.form.create_database.success'),
|
|
274
|
+
life: 3000,
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
emit('success', { database })
|
|
278
|
+
closeModal()
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
console.error('Failed to create knowledge database:', error)
|
|
282
|
+
toast.add({
|
|
283
|
+
severity: 'error',
|
|
284
|
+
summary: t('knowledge.form.create_database.error'),
|
|
285
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
286
|
+
life: 5000,
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
isSubmitting.value = false
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
</script>
|
|
294
|
+
|
|
295
|
+
<style scoped>
|
|
296
|
+
.content {
|
|
297
|
+
@apply font-light text-xs
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.content :deep(.formkit-group-fieldset) {
|
|
301
|
+
@apply flex flex-col gap-6;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.content :deep(.formkit-outer) {
|
|
305
|
+
@apply pt-3 pb-1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.content :deep(.formkit-group-fieldset legend) {
|
|
309
|
+
@apply hidden;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.content :deep(#create-database-form-incomplete) {
|
|
313
|
+
@apply font-bold text-sm text-right pr-2;
|
|
314
|
+
}
|
|
315
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
class="flex w-full cursor-pointer items-center gap-3 rounded-xl border-2 border-dashed border-surface-300 p-4 text-left hover:border-primary-500 hover:bg-surface-50 dark:border-surface-600 dark:hover:bg-surface-800"
|
|
5
|
+
@click="handleAdd"
|
|
6
|
+
>
|
|
7
|
+
<div class="flex items-center justify-center p-2">
|
|
8
|
+
<i
|
|
9
|
+
class="pi pi-database text-surface-400"
|
|
10
|
+
style="font-size: 1.5rem"
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
13
|
+
<div>
|
|
14
|
+
<h3 class="font-medium text-surface-600 dark:text-surface-400">
|
|
15
|
+
{{ t('knowledge.add_database.title') }}
|
|
16
|
+
</h3>
|
|
17
|
+
<p class="text-sm text-surface-500 dark:text-surface-400">
|
|
18
|
+
{{ t('knowledge.add_database.description') }}
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
</button>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
const { t } = useI18n()
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{
|
|
28
|
+
add: []
|
|
29
|
+
}>()
|
|
30
|
+
|
|
31
|
+
const handleAdd = () => {
|
|
32
|
+
emit('add')
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Dialog
|
|
3
|
+
:visible="visible"
|
|
4
|
+
modal
|
|
5
|
+
:header="title"
|
|
6
|
+
:style="{ width: '32rem' }"
|
|
7
|
+
:closable="!isDeleting"
|
|
8
|
+
@update:visible="emit('update:visible', $event)"
|
|
9
|
+
>
|
|
10
|
+
<div class="flex flex-col gap-4">
|
|
11
|
+
<Message
|
|
12
|
+
severity="warn"
|
|
13
|
+
:closable="false"
|
|
14
|
+
>
|
|
15
|
+
{{ warning }}
|
|
16
|
+
</Message>
|
|
17
|
+
|
|
18
|
+
<div class="flex flex-col gap-2">
|
|
19
|
+
<label
|
|
20
|
+
for="delete-confirm-input"
|
|
21
|
+
class="text-sm font-medium"
|
|
22
|
+
>
|
|
23
|
+
{{ t('knowledge.delete.confirm_label', { name: expectedName }) }}
|
|
24
|
+
</label>
|
|
25
|
+
<!-- Deliberately no placeholder: the label already names what to type, and putting it in the
|
|
26
|
+
field too turns a deliberate confirmation into copying the answer out of the box. -->
|
|
27
|
+
<InputText
|
|
28
|
+
id="delete-confirm-input"
|
|
29
|
+
v-model="typedName"
|
|
30
|
+
autocomplete="off"
|
|
31
|
+
:disabled="isDeleting"
|
|
32
|
+
@keyup.enter="confirmIfMatch"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<template #footer>
|
|
38
|
+
<div class="flex justify-end gap-2">
|
|
39
|
+
<Button
|
|
40
|
+
:label="t('knowledge.actions.cancel')"
|
|
41
|
+
severity="secondary"
|
|
42
|
+
outlined
|
|
43
|
+
:disabled="isDeleting"
|
|
44
|
+
@click="emit('update:visible', false)"
|
|
45
|
+
/>
|
|
46
|
+
<Button
|
|
47
|
+
:label="t('knowledge.delete.actions.delete')"
|
|
48
|
+
severity="danger"
|
|
49
|
+
:disabled="!nameMatches"
|
|
50
|
+
:loading="isDeleting"
|
|
51
|
+
@click="emit('confirm')"
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
</template>
|
|
55
|
+
</Dialog>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<script setup lang="ts">
|
|
59
|
+
const props = defineProps<{
|
|
60
|
+
visible: boolean
|
|
61
|
+
title: string
|
|
62
|
+
warning: string
|
|
63
|
+
expectedName: string
|
|
64
|
+
isDeleting?: boolean
|
|
65
|
+
}>()
|
|
66
|
+
|
|
67
|
+
const emit = defineEmits<{
|
|
68
|
+
'update:visible': [value: boolean]
|
|
69
|
+
'confirm': []
|
|
70
|
+
}>()
|
|
71
|
+
|
|
72
|
+
const { t } = useI18n()
|
|
73
|
+
|
|
74
|
+
const typedName = ref('')
|
|
75
|
+
|
|
76
|
+
const nameMatches = computed(() => typedName.value.trim() === props.expectedName)
|
|
77
|
+
|
|
78
|
+
const confirmIfMatch = () => {
|
|
79
|
+
if (nameMatches.value && !props.isDeleting) emit('confirm')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Reset the typed name whenever the dialog opens for a different target, so a previous confirmation
|
|
83
|
+
// can never carry over and pre-satisfy the guard.
|
|
84
|
+
watch(() => props.visible, (isVisible) => {
|
|
85
|
+
if (isVisible) typedName.value = ''
|
|
86
|
+
})
|
|
87
|
+
</script>
|
|
@@ -45,6 +45,16 @@
|
|
|
45
45
|
severity="secondary"
|
|
46
46
|
@click.stop="handleUploadClick"
|
|
47
47
|
/>
|
|
48
|
+
<Button
|
|
49
|
+
v-if="!autoSync"
|
|
50
|
+
v-tooltip.top="t('knowledge.delete_namespace')"
|
|
51
|
+
icon="pi pi-trash"
|
|
52
|
+
rounded
|
|
53
|
+
text
|
|
54
|
+
size="small"
|
|
55
|
+
severity="danger"
|
|
56
|
+
@click.stop="handleDeleteClick"
|
|
57
|
+
/>
|
|
48
58
|
<Badge
|
|
49
59
|
:value="namespace.number_of_documents"
|
|
50
60
|
size="large"
|
|
@@ -67,6 +77,8 @@ import { capitalCase } from 'change-case'
|
|
|
67
77
|
|
|
68
78
|
import type { NamespaceDto } from '@core/sdk/client'
|
|
69
79
|
|
|
80
|
+
// Not gated on the database's `deletable`: a legacy database cannot be removed as a whole, but its
|
|
81
|
+
// namespaces can. Only an auto-synced source, which would just re-sync them, blocks both affordances.
|
|
70
82
|
const props = defineProps<{
|
|
71
83
|
namespace: NamespaceDto
|
|
72
84
|
autoSync?: boolean
|
|
@@ -75,6 +87,7 @@ const props = defineProps<{
|
|
|
75
87
|
const emit = defineEmits<{
|
|
76
88
|
upload: [namespace: NamespaceDto]
|
|
77
89
|
edit: [namespace: NamespaceDto]
|
|
90
|
+
delete: [namespace: NamespaceDto]
|
|
78
91
|
}>()
|
|
79
92
|
|
|
80
93
|
const route = useRoute()
|
|
@@ -105,4 +118,9 @@ const handleEditClick = (event: Event) => {
|
|
|
105
118
|
event.stopPropagation()
|
|
106
119
|
emit('edit', props.namespace)
|
|
107
120
|
}
|
|
121
|
+
|
|
122
|
+
const handleDeleteClick = (event: Event) => {
|
|
123
|
+
event.stopPropagation()
|
|
124
|
+
emit('delete', props.namespace)
|
|
125
|
+
}
|
|
108
126
|
</script>
|
|
@@ -190,9 +190,17 @@ const handleCreate = async () => {
|
|
|
190
190
|
tenantId: tenantId.value!,
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
try {
|
|
194
|
+
await createNamespace(requestBody)
|
|
195
|
+
emit('success', { database: selectedDatabase.value, namespace: name.value })
|
|
196
|
+
closeModal()
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
error.value = t('knowledge.form.create_error')
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
isCreating.value = false
|
|
203
|
+
}
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
watch(() => props.modelValue, (isVisible) => {
|