@swiss-ai-hub/web 0.321.0 → 0.323.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.
Files changed (100) hide show
  1. package/components/Event/Display/AddMemoryToChatHistoryEvent.vue +3 -3
  2. package/components/Event/Display/AttachedFileEvent.vue +42 -0
  3. package/components/Event/Display/AttachedFilesReadEvent.vue +22 -0
  4. package/components/Event/Display/BaseRetrieveMemoryEvent.vue +2 -2
  5. package/components/Event/Display/BaseStoreMemoryEvent.vue +2 -2
  6. package/components/Event/Display/CompleteConversationEvent.vue +35 -0
  7. package/components/Event/Display/ComposeContextEvent.vue +38 -0
  8. package/components/Event/Display/ContextComposedEvent.vue +60 -0
  9. package/components/Event/Display/ContextualizeConversationEvent.vue +26 -0
  10. package/components/Event/Display/ConversationContextualizedEvent.vue +34 -0
  11. package/components/Event/Display/HumanInTheLoopResponseEvent.vue +11 -1
  12. package/components/Event/Display/KnowledgeSearchedEvent.vue +33 -0
  13. package/components/Event/Display/MemoryRecalledEvent.vue +33 -0
  14. package/components/Event/Display/MemoryStorageRequestedEvent.vue +29 -0
  15. package/components/Event/Display/NotAMetaQuestionEvent.vue +20 -0
  16. package/components/Event/Display/Part/Facts.vue +39 -0
  17. package/components/Event/Display/Part/MessageList.vue +50 -0
  18. package/components/Event/Display/Part/ToolCalls.vue +58 -0
  19. package/components/Event/Display/Part/ToolLoopState.vue +36 -0
  20. package/components/Event/Display/ReadAttachedFilesEvent.vue +28 -0
  21. package/components/Event/Display/RecallMemoryEvent.vue +27 -0
  22. package/components/Event/Display/RunToolLoopEvent.vue +34 -0
  23. package/components/Event/Display/SandboxFileDisplayedEvent.vue +46 -0
  24. package/components/Event/Display/SearchKnowledgeEvent.vue +31 -0
  25. package/components/Event/Display/ToolCallApprovedEvent.vue +27 -0
  26. package/components/Event/Display/ToolCallsDecidedEvent.vue +24 -0
  27. package/components/Event/Display/ToolLoopCondensedEvent.vue +31 -0
  28. package/components/Event/Display/ToolLoopFinishedEvent.vue +49 -0
  29. package/components/Event/Display/ToolLoopIterationEvent.vue +18 -0
  30. package/components/Event/Display/ToolLoopStatusEvent.vue +27 -0
  31. package/components/Event/Display/ToolResultEvent.vue +38 -0
  32. package/components/Event/List/EventList.vue +1 -1
  33. package/components/FormKit/DynamicConfiguration.vue +6 -1
  34. package/components/FormKit/KnowledgeCollectionSelector.vue +281 -0
  35. package/components/FormKit/KnowledgeDatabaseSelector.vue +7 -0
  36. package/components/FormKit/LocaleInput.vue +2 -0
  37. package/components/FormKit/SecretFileInput.vue +181 -0
  38. package/components/FormKit/VectorStoreInput.vue +20 -1
  39. package/components/Knowledge/Database/CreateModal.vue +91 -2
  40. package/components/Knowledge/Database/SourceModal.vue +165 -0
  41. package/components/Knowledge/Document/UploadModal.vue +4 -0
  42. package/components/Knowledge/Namespace/Card.vue +4 -4
  43. package/components/Knowledge/Namespace/CreateModal.vue +1 -1
  44. package/components/Knowledge/Namespace/EditModal.vue +3 -4
  45. package/components/Role/AccessRulesEditor.vue +1 -1
  46. package/components/Service/Selection.vue +7 -1
  47. package/components/Support/IncidentButton.vue +37 -0
  48. package/components/Support/IncidentDialog.vue +119 -0
  49. package/components/User/Settings.vue +9 -8
  50. package/components/UserFiles/Browser.vue +361 -0
  51. package/components/UserFiles/MoveDialog.vue +77 -0
  52. package/components/UserFiles/NameDialog.vue +59 -0
  53. package/components/UserFiles/Preview.vue +110 -0
  54. package/composables/auth/useAuth.ts +19 -3
  55. package/composables/document/useDocumentIngestionPolling.ts +64 -0
  56. package/composables/document/useDocuments.ts +3 -0
  57. package/composables/document/useSourcePipelines.ts +23 -0
  58. package/composables/document/useUpdateDatabaseSource.ts +21 -0
  59. package/composables/event/useEventComponent.ts +46 -0
  60. package/composables/form/useFormKitTransform.ts +68 -17
  61. package/composables/openwebui/useOpenWebuiDisclaimer.ts +22 -0
  62. package/composables/suite/useApps.ts +12 -3
  63. package/composables/suite/useSuite.ts +2 -0
  64. package/composables/support/useIncidentAvailability.ts +17 -0
  65. package/composables/support/useIncidentContext.ts +56 -0
  66. package/composables/support/useIncidentForm.ts +45 -0
  67. package/composables/support/useIncidentReport.ts +16 -0
  68. package/composables/support/useOpenWebUIContext.ts +38 -0
  69. package/composables/tenant/useChatDisclaimer.ts +24 -0
  70. package/composables/user/useApplyLocale.ts +23 -0
  71. package/composables/user/useMyLocale.ts +20 -0
  72. package/composables/user/useRestorePreferredLocale.ts +38 -0
  73. package/composables/userFiles/useCreateUserFolder.ts +15 -0
  74. package/composables/userFiles/useDeleteUserFile.ts +15 -0
  75. package/composables/userFiles/useMoveUserFile.ts +15 -0
  76. package/composables/userFiles/useUploadUserFiles.ts +19 -0
  77. package/composables/userFiles/useUserFileContent.ts +24 -0
  78. package/composables/userFiles/useUserFiles.ts +25 -0
  79. package/formkit.config.ts +10 -2
  80. package/i18n/locales/de.yaml +171 -3
  81. package/i18n/locales/en.yaml +172 -3
  82. package/i18n/locales/fr.yaml +173 -3
  83. package/i18n/locales/it.yaml +173 -3
  84. package/layouts/default.vue +7 -1
  85. package/middleware/auth.global.ts +6 -3
  86. package/middleware/home-redirect.ts +15 -4
  87. package/package.json +4 -3
  88. package/pages/[tenant]/service/knowledge/[db]/[namespace].vue +6 -8
  89. package/pages/[tenant]/service/knowledge.vue +33 -7
  90. package/pages/[tenant]/service/openai/[thread_id]/[display_id]/files.vue +51 -0
  91. package/pages/[tenant]/service/openai.vue +61 -17
  92. package/pages/[tenant]/service/user-knowledge.vue +19 -0
  93. package/plugins/api-client.client.ts +5 -0
  94. package/plugins/oidc-client.ts +14 -7
  95. package/sdk/client/index.ts +171 -1
  96. package/sdk/client/schemas.gen.ts +18602 -8560
  97. package/sdk/client/sdk.gen.ts +482 -0
  98. package/sdk/client/types.gen.ts +13616 -7044
  99. package/types/EventFact.ts +4 -0
  100. package/utils/apiResponseGuard.ts +97 -0
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mdi:brain"
6
+ >
7
+ <EventDisplayPartFacts :facts="facts" />
8
+ </EventDisplayBase>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import type { ContextualizedAgentEvent, RecallMemoryEvent, ThreadDto } from '@core/sdk/client'
13
+ import type { EventFact } from '@core/types/EventFact'
14
+
15
+ const props = defineProps<{
16
+ event: ContextualizedAgentEvent & { event: RecallMemoryEvent }
17
+ thread: ThreadDto
18
+ }>()
19
+
20
+ const { t } = useI18n()
21
+
22
+ const facts = computed<EventFact[]>(() => [
23
+ { label: t('event.capability.query'), value: props.event.event.query },
24
+ { label: t('event.capability.namespaces'), value: props.event.event.org_memory_namespaces ?? [] },
25
+ { label: t('event.capability.toolCall'), value: props.event.event.tool_call_id },
26
+ ])
27
+ </script>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mynaui:tool"
6
+ >
7
+ <EventDisplayPartFacts :facts="facts" />
8
+ </EventDisplayBase>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import type { ContextualizedAgentEvent, RunToolLoopEvent, ThreadDto } from '@core/sdk/client'
13
+ import type { EventFact } from '@core/types/EventFact'
14
+
15
+ const props = defineProps<{
16
+ event: ContextualizedAgentEvent & { event: RunToolLoopEvent }
17
+ thread: ThreadDto
18
+ }>()
19
+
20
+ const { t } = useI18n()
21
+
22
+ const facts = computed<EventFact[]>(() => [
23
+ { label: t('event.toolLoop.loop'), value: props.event.event.loop },
24
+ { label: t('event.toolLoop.mode'), value: t(`event.toolLoop.modes.${props.event.event.mode ?? 'answer'}`) },
25
+ { label: t('event.toolLoop.narrowedTools'), value: props.event.event.tools ?? null },
26
+ { label: t('event.toolLoop.maxIterations'), value: props.event.event.max_iterations },
27
+ { label: t('event.capability.historyMessages'), value: (props.event.event.history ?? []).length },
28
+ { label: t('event.capability.files'), value: (props.event.event.files ?? []).map(file => file.filename) },
29
+ {
30
+ label: t('event.capability.references'),
31
+ value: (props.event.event.knowledge_references ?? []).map(reference => `${reference.database} / ${reference.namespace}`),
32
+ },
33
+ ])
34
+ </script>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mdi:file-eye-outline"
6
+ >
7
+ <dl class="grid grid-cols-[max-content_1fr] gap-x-4 gap-y-1 text-sm">
8
+ <dt class="text-surface-500">
9
+ {{ t('event.sandboxFileDisplayed.file') }}
10
+ </dt>
11
+ <dd class="break-all font-medium">
12
+ {{ event.event.filename }}
13
+ </dd>
14
+ <dt class="text-surface-500">
15
+ {{ t('event.sandboxFileDisplayed.type') }}
16
+ </dt>
17
+ <dd>{{ event.event.content_type }}</dd>
18
+ <dt class="text-surface-500">
19
+ {{ t('event.sandboxFileDisplayed.size') }}
20
+ </dt>
21
+ <dd>{{ size }}</dd>
22
+ <dt class="text-surface-500">
23
+ {{ t('event.sandboxFileDisplayed.path') }}
24
+ </dt>
25
+ <dd class="break-all font-mono text-xs">
26
+ {{ event.event.path }}
27
+ </dd>
28
+ </dl>
29
+ </EventDisplayBase>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import type { SandboxFileDisplayedEvent, ThreadDto, ContextualizedAgentEvent } from '@core/sdk/client'
34
+
35
+ const props = defineProps<{
36
+ event: ContextualizedAgentEvent & { event: SandboxFileDisplayedEvent }
37
+ thread: ThreadDto
38
+ }>()
39
+
40
+ const { t, n } = useI18n()
41
+
42
+ const size = computed<string>(() => {
43
+ const bytes = props.event.event.size
44
+ return bytes < 1024 ? `${bytes} B` : `${n(Math.round(bytes / 102.4) / 10)} KB`
45
+ })
46
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mage:search"
6
+ >
7
+ <EventDisplayPartFacts :facts="facts" />
8
+ </EventDisplayBase>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import type { ContextualizedAgentEvent, SearchKnowledgeEvent, ThreadDto } from '@core/sdk/client'
13
+ import type { EventFact } from '@core/types/EventFact'
14
+
15
+ const props = defineProps<{
16
+ event: ContextualizedAgentEvent & { event: SearchKnowledgeEvent }
17
+ thread: ThreadDto
18
+ }>()
19
+
20
+ const { t } = useI18n()
21
+
22
+ const facts = computed<EventFact[]>(() => [
23
+ { label: t('event.capability.query'), value: props.event.event.query },
24
+ {
25
+ label: t('event.capability.references'),
26
+ value: (props.event.event.references ?? []).map(reference => `${reference.database} / ${reference.namespace}`),
27
+ },
28
+ { label: t('event.capability.citeSources'), value: t(props.event.event.cite_sources ? 'event.parts.yes' : 'event.parts.no') },
29
+ { label: t('event.capability.toolCall'), value: props.event.event.tool_call_id },
30
+ ])
31
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mynaui:tool"
6
+ >
7
+ <EventDisplayPartFacts :facts="facts" />
8
+ </EventDisplayBase>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import type { ContextualizedAgentEvent, ThreadDto, ToolCallApprovedEvent } from '@core/sdk/client'
13
+ import type { EventFact } from '@core/types/EventFact'
14
+
15
+ const props = defineProps<{
16
+ event: ContextualizedAgentEvent & { event: ToolCallApprovedEvent }
17
+ thread: ThreadDto
18
+ }>()
19
+
20
+ const { t } = useI18n()
21
+
22
+ const facts = computed<EventFact[]>(() => [
23
+ { label: t('event.toolLoop.tool'), value: props.event.event.name },
24
+ { label: t('event.toolLoop.kind'), value: t(`event.toolLoop.kinds.${props.event.event.kind}`) },
25
+ { label: t('event.capability.toolCall'), value: props.event.event.tool_call_id },
26
+ ])
27
+ </script>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mynaui:tool"
6
+ >
7
+ <div class="flex flex-col gap-6">
8
+ <EventDisplayPartToolCalls
9
+ :messages="event.event.state.messages"
10
+ :tool-call-ids="event.event.tool_call_ids"
11
+ />
12
+ <EventDisplayPartToolLoopState :state="event.event.state" />
13
+ </div>
14
+ </EventDisplayBase>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import type { ContextualizedAgentEvent, ThreadDto, ToolCallsDecidedEvent } from '@core/sdk/client'
19
+
20
+ defineProps<{
21
+ event: ContextualizedAgentEvent & { event: ToolCallsDecidedEvent }
22
+ thread: ThreadDto
23
+ }>()
24
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="lucide:layers"
6
+ >
7
+ <EventDisplayPartFacts :facts="facts" />
8
+ </EventDisplayBase>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import type { ContextualizedAgentEvent, ThreadDto, ToolLoopCondensedEvent } from '@core/sdk/client'
13
+ import type { EventFact } from '@core/types/EventFact'
14
+
15
+ const props = defineProps<{
16
+ event: ContextualizedAgentEvent & { event: ToolLoopCondensedEvent }
17
+ thread: ThreadDto
18
+ }>()
19
+
20
+ const { t } = useI18n()
21
+
22
+ const facts = computed<EventFact[]>(() => [
23
+ { label: t('event.toolLoop.loop'), value: props.event.event.loop },
24
+ {
25
+ label: t('event.toolLoop.tokens'),
26
+ value: `${props.event.event.tokens_before} → ${props.event.event.tokens_after}`,
27
+ },
28
+ { label: t('event.toolLoop.condensedResults'), value: props.event.event.condensed_results ?? 0 },
29
+ { label: t('event.toolLoop.condensedTurns'), value: props.event.event.condensed_turns ?? 0 },
30
+ ])
31
+ </script>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mynaui:tool"
6
+ >
7
+ <div class="flex flex-col gap-4">
8
+ <EventDisplayPartFacts :facts="facts" />
9
+ <Tag
10
+ v-if="event.event.stopped_early"
11
+ :value="t('event.toolLoop.stoppedEarly')"
12
+ severity="warn"
13
+ class="self-start"
14
+ />
15
+ <p
16
+ v-if="answer"
17
+ class="whitespace-pre-wrap rounded-xl bg-surface-50 p-3 text-sm dark:bg-surface-850"
18
+ >
19
+ {{ answer }}
20
+ </p>
21
+ <EventDisplayPartMessageList
22
+ v-if="(event.event.block ?? []).length > 0"
23
+ :messages="event.event.block ?? []"
24
+ :event="event"
25
+ :thread="thread"
26
+ :title="t('event.toolLoop.gathered')"
27
+ />
28
+ </div>
29
+ </EventDisplayBase>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import type { ContextualizedAgentEvent, ThreadDto, ToolLoopFinishedEvent } from '@core/sdk/client'
34
+ import type { EventFact } from '@core/types/EventFact'
35
+
36
+ const props = defineProps<{
37
+ event: ContextualizedAgentEvent & { event: ToolLoopFinishedEvent }
38
+ thread: ThreadDto
39
+ }>()
40
+
41
+ const { t } = useI18n()
42
+
43
+ const answer = computed(() => props.event.event.answer?.output_messages?.at(-1)?.content)
44
+
45
+ const facts = computed<EventFact[]>(() => [
46
+ { label: t('event.toolLoop.loop'), value: props.event.event.loop },
47
+ { label: t('event.toolLoop.gatheredMessages'), value: (props.event.event.block ?? []).length || null },
48
+ ])
49
+ </script>
@@ -0,0 +1,18 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mynaui:tool"
6
+ >
7
+ <EventDisplayPartToolLoopState :state="event.event.state" />
8
+ </EventDisplayBase>
9
+ </template>
10
+
11
+ <script setup lang="ts">
12
+ import type { ContextualizedAgentEvent, ThreadDto, ToolLoopIterationEvent } from '@core/sdk/client'
13
+
14
+ defineProps<{
15
+ event: ContextualizedAgentEvent & { event: ToolLoopIterationEvent }
16
+ thread: ThreadDto
17
+ }>()
18
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mynaui:tool"
6
+ >
7
+ <div class="flex items-center gap-2 text-sm">
8
+ <Icon
9
+ :name="event.event.done ? 'mage:check-circle' : 'mage:loading'"
10
+ class="size-4"
11
+ />
12
+ <span>{{ event.event.description }}</span>
13
+ <span class="text-xs text-surface-500 dark:text-surface-400">{{ t('event.toolLoop.loop') }}: {{ event.event.loop }}</span>
14
+ </div>
15
+ </EventDisplayBase>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import type { ContextualizedAgentEvent, ThreadDto, ToolLoopStatusEvent } from '@core/sdk/client'
20
+
21
+ defineProps<{
22
+ event: ContextualizedAgentEvent & { event: ToolLoopStatusEvent }
23
+ thread: ThreadDto
24
+ }>()
25
+
26
+ const { t } = useI18n()
27
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <EventDisplayBase
3
+ :event="event"
4
+ :thread="thread"
5
+ icon="mynaui:tool"
6
+ >
7
+ <div class="flex flex-col gap-4">
8
+ <div class="flex items-center gap-2">
9
+ <span class="font-semibold">{{ event.event.name }}</span>
10
+ <Tag
11
+ v-if="event.event.is_error"
12
+ :value="t('event.toolLoop.failed')"
13
+ severity="danger"
14
+ />
15
+ <span class="text-xs text-surface-500 dark:text-surface-400">{{ event.event.tool_call_id }}</span>
16
+ </div>
17
+ <pre class="max-h-96 overflow-auto whitespace-pre-wrap break-words rounded-xl bg-surface-50 p-3 text-xs dark:bg-surface-850">{{ event.event.content }}</pre>
18
+ <EventDisplayPartMessageList
19
+ v-if="(event.event.block ?? []).length > 0"
20
+ :messages="event.event.block ?? []"
21
+ :event="event"
22
+ :thread="thread"
23
+ :title="t('event.toolLoop.resultContext')"
24
+ />
25
+ </div>
26
+ </EventDisplayBase>
27
+ </template>
28
+
29
+ <script setup lang="ts">
30
+ import type { ContextualizedAgentEvent, ThreadDto, ToolResultEvent } from '@core/sdk/client'
31
+
32
+ defineProps<{
33
+ event: ContextualizedAgentEvent & { event: ToolResultEvent }
34
+ thread: ThreadDto
35
+ }>()
36
+
37
+ const { t } = useI18n()
38
+ </script>
@@ -144,7 +144,7 @@ const { resolveComponentForEvent } = useEventComponent()
144
144
 
145
145
  const runNameFn = (run: RunStatistics) => run.agent.agent_config.name
146
146
 
147
- const getEventTimestamp = (event: AgentEventReadable): number =>
147
+ const getEventTimestamp = (event: ContextualizedAgentEvent): number =>
148
148
  (event.event as { created_at?: number }).created_at ?? 0
149
149
 
150
150
  const sortedEvents = computed(() =>
@@ -4,7 +4,7 @@
4
4
  id="form"
5
5
  v-model="data"
6
6
  type="form"
7
- :submit-label="t('common.actions.save')"
7
+ :submit-label="props.submitLabel ?? t('common.actions.save')"
8
8
  :submit-attrs="{
9
9
  inputClass: 'p-button p-component w-full',
10
10
  }"
@@ -32,6 +32,9 @@
32
32
  :max="rep.max"
33
33
  @update:model-value="setRepeaterData(rep.path, $event)"
34
34
  />
35
+
36
+ <!-- Inside the form so it lands above the submit button, which FormKit renders last. -->
37
+ <slot name="before-submit" />
35
38
  </FormKit>
36
39
  </div>
37
40
  </template>
@@ -64,6 +67,8 @@ const FIELD_WARNING_KEYS: Record<string, string> = {
64
67
  const props = defineProps<{
65
68
  form: FormkitElement[]
66
69
  initialData?: Record<string, unknown>
70
+ // Every caller so far is saving a configuration; the incident form is sending a report.
71
+ submitLabel?: string
67
72
  }>()
68
73
 
69
74
  // Clone so the form model never shares references with the Pinia-Colada cache: otherwise
@@ -0,0 +1,281 @@
1
+ <template>
2
+ <div class="flex flex-col gap-1">
3
+ <MultiSelect
4
+ v-model="selectedKeys"
5
+ :input-id="inputId"
6
+ :options="groupedOptions"
7
+ option-group-label="label"
8
+ option-group-children="items"
9
+ option-label="displayName"
10
+ option-value="key"
11
+ :placeholder="placeholder ?? t('lib.knowledgeCollections.placeholder')"
12
+ :filter="filter"
13
+ :loading="isLoading"
14
+ :disabled="!hasAgent"
15
+ display="chip"
16
+ class="w-full"
17
+ >
18
+ <!-- PrimeVue 4.5 renders the chip-mode placeholder twice while loading with no options yet. -->
19
+ <template
20
+ v-if="currentValue.length === 0"
21
+ #value="{ placeholder: emptyLabel }"
22
+ >
23
+ {{ emptyLabel }}
24
+ </template>
25
+ <template #optiongroup="{ option }">
26
+ <div class="flex items-center gap-2">
27
+ <Icon
28
+ name="mage:database"
29
+ size="1.2em"
30
+ />
31
+ <span>{{ option.label }}</span>
32
+ </div>
33
+ </template>
34
+ <template #option="{ option }">
35
+ <div class="flex items-center gap-2">
36
+ <Icon
37
+ name="mage:folder"
38
+ size="1.2em"
39
+ />
40
+ <span>{{ option.displayName }}</span>
41
+ </div>
42
+ </template>
43
+ </MultiSelect>
44
+ <small class="text-surface-500">
45
+ {{ hint }}
46
+ </small>
47
+ </div>
48
+ </template>
49
+
50
+ <script setup lang="ts">
51
+ import { getAgentInstance, getDatabases } from '@core/sdk/client'
52
+
53
+ import type { DatabaseDto } from '@core/sdk/client'
54
+ import type { FormKitNode } from '@formkit/core'
55
+
56
+ /**
57
+ * A collection is only addressable as a (database, collection) pair — the same name may exist in more
58
+ * than one database — so the form value is a list of pairs, the shape `RAGStartEvent.selected_namespaces`
59
+ * takes. PrimeVue selects on a primitive, hence the `database/collection` key used inside this component.
60
+ */
61
+ interface BucketNamespacePair {
62
+ bucket_name: string
63
+ namespace_name: string
64
+ }
65
+
66
+ interface AgentRefValue {
67
+ agent_class?: string | null
68
+ agent_id?: string | null
69
+ }
70
+
71
+ /** One retriever's vector store, as it is saved in the referenced agent's configuration. */
72
+ interface VectorStoreConfig {
73
+ collection_name?: string
74
+ index_namespaces?: string[]
75
+ all_namespaces?: boolean
76
+ }
77
+
78
+ interface CollectionOption {
79
+ key: string
80
+ displayName: string
81
+ pair: BucketNamespacePair
82
+ }
83
+
84
+ interface CollectionGroup {
85
+ label: string
86
+ items: CollectionOption[]
87
+ }
88
+
89
+ interface KnowledgeCollectionSelectorProps {
90
+ context: {
91
+ node: {
92
+ input: (value: BucketNamespacePair[] | null) => void
93
+ at: (address: string) => FormKitNode | null | undefined
94
+ }
95
+ value?: BucketNamespacePair[] | null
96
+ attrs: Record<string, unknown>
97
+ id?: string
98
+ agentRef?: string
99
+ placeholder?: string
100
+ filter?: boolean
101
+ }
102
+ }
103
+
104
+ const props = defineProps<KnowledgeCollectionSelectorProps>()
105
+ const { t } = useI18n()
106
+ const { tenantId } = useTenant()
107
+
108
+ const agentRefPath = computed(() => props.context.agentRef)
109
+ const placeholder = computed(() => props.context.placeholder)
110
+ const filter = computed(() => props.context.filter ?? true)
111
+ const inputId = computed(() => `${props.context.id ?? 'knowledge-collections'}-select`)
112
+
113
+ const agentRef = ref<AgentRefValue | null>(null)
114
+ const collections = ref<BucketNamespacePair[]>([])
115
+ const databaseNames = ref<Record<string, string>>({})
116
+ const collectionNames = ref<Record<string, string>>({})
117
+ const isLoading = ref(false)
118
+
119
+ const hasAgent = computed(() => !!agentRef.value?.agent_class && !!agentRef.value?.agent_id)
120
+
121
+ const currentValue = computed<BucketNamespacePair[]>(() => props.context.value ?? [])
122
+
123
+ function keyOf(pair: BucketNamespacePair): string {
124
+ return `${pair.bucket_name}/${pair.namespace_name}`
125
+ }
126
+
127
+ function collectionLabel(pair: BucketNamespacePair): string {
128
+ return collectionNames.value[keyOf(pair)] || pair.namespace_name
129
+ }
130
+
131
+ const groupedOptions = computed<CollectionGroup[]>(() => {
132
+ const byDatabase = new Map<string, CollectionOption[]>()
133
+ for (const pair of collections.value) {
134
+ const options = byDatabase.get(pair.bucket_name) ?? []
135
+ options.push({
136
+ key: keyOf(pair),
137
+ displayName: collectionLabel(pair),
138
+ pair,
139
+ })
140
+ byDatabase.set(pair.bucket_name, options)
141
+ }
142
+ const groups = [...byDatabase.entries()].map(([database, items]) => ({
143
+ label: databaseNames.value[database] || database,
144
+ items,
145
+ }))
146
+
147
+ // PrimeVue labels a chip from the matching option, so a selection the agent does not retrieve from
148
+ // would otherwise render as its remove icon and nothing else. It is listed under its own group,
149
+ // qualified by database because that is usually what went wrong — the selection was made against a
150
+ // different agent — and so the narrowing the run still applies stays visible enough to act on.
151
+ const available = new Set(collections.value.map(keyOf))
152
+ const unavailable = currentValue.value.filter(pair => !available.has(keyOf(pair)))
153
+ if (unavailable.length === 0) return groups
154
+
155
+ return [...groups, {
156
+ label: t('lib.knowledgeCollections.unavailable'),
157
+ items: unavailable.map(pair => ({
158
+ key: keyOf(pair),
159
+ displayName: `${databaseNames.value[pair.bucket_name] || pair.bucket_name} / ${collectionLabel(pair)}`,
160
+ pair,
161
+ })),
162
+ }]
163
+ })
164
+
165
+ // A selection saved before the referenced agent lost a collection would silently disappear from the
166
+ // chips on load, so it stays listed until the user changes the selection themselves.
167
+ const selectedKeys = computed({
168
+ get: () => currentValue.value.map(keyOf),
169
+ set: (keys: string[]) => {
170
+ const byKey = new Map(collections.value.map(pair => [keyOf(pair), pair]))
171
+ props.context.node.input(keys.map(key => byKey.get(key) ?? currentValue.value.find(pair => keyOf(pair) === key))
172
+ .filter((pair): pair is BucketNamespacePair => !!pair))
173
+ },
174
+ })
175
+
176
+ const hint = computed(() => {
177
+ if (!agentRefPath.value) return t('lib.knowledgeCollections.noSource')
178
+ if (!hasAgent.value) return t('lib.knowledgeCollections.noAgent')
179
+ if (!isLoading.value && collections.value.length === 0) return t('lib.knowledgeCollections.empty')
180
+ return t('lib.knowledgeCollections.help')
181
+ })
182
+
183
+ /**
184
+ * The collections the referenced agent actually retrieves from: the namespaces each of its retrievers
185
+ * names, or — for a retriever scoped to a whole database — every namespace that database holds. Anything
186
+ * outside this set would be dropped at runtime and the reply answered from nothing, which is why the
187
+ * catalogue alone is not offered here.
188
+ */
189
+ function collectionsFromRetrievers(configuration: Record<string, unknown>, databases: DatabaseDto[]): BucketNamespacePair[] {
190
+ const retrievers = Array.isArray(configuration.retrievers) ? configuration.retrievers : []
191
+ const pairs: BucketNamespacePair[] = []
192
+ const seen = new Set<string>()
193
+
194
+ for (const retriever of retrievers) {
195
+ const store = (retriever as Record<string, unknown>)?.vector_store as VectorStoreConfig | undefined
196
+ const database = store?.collection_name
197
+ if (!database) continue
198
+
199
+ const namespaces = store?.all_namespaces
200
+ ? (databases.find(entry => entry.name === database)?.namespaces ?? []).map(namespace => namespace.name)
201
+ : (store?.index_namespaces ?? [])
202
+
203
+ for (const namespace of namespaces) {
204
+ const pair = { bucket_name: database, namespace_name: namespace }
205
+ const key = keyOf(pair)
206
+ if (seen.has(key)) continue
207
+ seen.add(key)
208
+ pairs.push(pair)
209
+ }
210
+ }
211
+ return pairs
212
+ }
213
+
214
+ async function loadCollections() {
215
+ const agentClass = agentRef.value?.agent_class
216
+ const agentId = agentRef.value?.agent_id
217
+ if (!agentClass || !agentId) {
218
+ collections.value = []
219
+ return
220
+ }
221
+
222
+ isLoading.value = true
223
+ try {
224
+ const [instance, databases] = await Promise.all([
225
+ getAgentInstance({
226
+ composable: '$fetch',
227
+ path: { tenant_id: tenantId.value!, agent_class: agentClass, agent_id: agentId },
228
+ }),
229
+ getDatabases({ composable: '$fetch', path: { tenant_id: tenantId.value! } }),
230
+ ])
231
+ databaseNames.value = Object.fromEntries(
232
+ databases.map(database => [database.name, database.display_name || database.name]),
233
+ )
234
+ collectionNames.value = Object.fromEntries(
235
+ databases.flatMap(database => database.namespaces
236
+ .filter(namespace => namespace.display_name)
237
+ .map(namespace => [keyOf({ bucket_name: database.name, namespace_name: namespace.name }), namespace.display_name!])),
238
+ )
239
+ collections.value = collectionsFromRetrievers(instance.configuration ?? {}, databases)
240
+ }
241
+ catch (error) {
242
+ console.error(`Failed to load knowledge collections for "${agentClass}/${agentId}":`, error)
243
+ collections.value = []
244
+ }
245
+ finally {
246
+ isLoading.value = false
247
+ }
248
+ }
249
+
250
+ /**
251
+ * The path arrives relative to the form root and is anchored here, never on the backend: FormKit compiles any
252
+ * schema string beginning with `$` as an expression, so a path shipped as `$root.…` is evaluated against the form
253
+ * data and reaches this component as `undefined`.
254
+ */
255
+ function readAgentRef(): AgentRefValue | null {
256
+ if (!agentRefPath.value) return null
257
+ return (props.context.node.at(`$root.${agentRefPath.value}`)?.value as AgentRefValue | undefined) ?? null
258
+ }
259
+
260
+ function syncAgentRef() {
261
+ const next = readAgentRef()
262
+ if (next?.agent_class === agentRef.value?.agent_class && next?.agent_id === agentRef.value?.agent_id) return
263
+ agentRef.value = next
264
+ loadCollections()
265
+ }
266
+
267
+ // The agent lives elsewhere on the form — a sibling group here, a different step in other layouts — so the
268
+ // only dependable signal that it changed is the root form committing. Values on FormKit nodes are not Vue
269
+ // reactive, and re-reading on every commit is cheap because `syncAgentRef` compares before refetching.
270
+ let rootNode: FormKitNode | null | undefined
271
+ let receipt: string | undefined
272
+ onMounted(() => {
273
+ syncAgentRef()
274
+ rootNode = props.context.node.at('$root')
275
+ receipt = rootNode?.on('commit', syncAgentRef)
276
+ })
277
+
278
+ onUnmounted(() => {
279
+ if (receipt) rootNode?.off(receipt)
280
+ })
281
+ </script>