feat(email): suggest entity links with AI when opening messages
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 48s
Build and Push Docker Images / build-frontend (push) Successful in 1m15s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-api (push) Successful in 21s
Build and Push Docker Images / build-central-services-admin (push) Successful in 20s
Build and Push Docker Images / build-docs (push) Successful in 1m17s
All checks were successful
Build and Push Docker Images / build-backend (push) Successful in 48s
Build and Push Docker Images / build-frontend (push) Successful in 1m15s
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-api (push) Successful in 21s
Build and Push Docker Images / build-central-services-admin (push) Successful in 20s
Build and Push Docker Images / build-docs (push) Successful in 1m17s
This commit is contained in:
@@ -23,6 +23,39 @@ const selectedEntityId = ref<number | null>(null)
|
||||
const entityOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
const loadingOptions = ref(false)
|
||||
const saving = ref(false)
|
||||
type Suggestion = Omit<EntityLink, "id"> & { confidence: "high" | "medium"; reason: string }
|
||||
const suggestions = ref<Suggestion[]>([])
|
||||
const loadingSuggestions = ref(false)
|
||||
const suggestionError = ref("")
|
||||
let suggestionRequest = 0
|
||||
let active = true
|
||||
onBeforeUnmount(() => { active = false; suggestionRequest++ })
|
||||
const visibleSuggestions = computed(() => suggestions.value.filter(suggestion =>
|
||||
!links.value.some(link => link.entityType === suggestion.entityType && link.entityId === suggestion.entityId),
|
||||
))
|
||||
|
||||
async function loadSuggestions(refresh = false) {
|
||||
const request = ++suggestionRequest
|
||||
const messageId = props.messageId
|
||||
loadingSuggestions.value = true
|
||||
suggestionError.value = ""
|
||||
try {
|
||||
const response = await useNuxtApp().$api(`/api/email/messages/${messageId}/entity-suggestions`, {
|
||||
method: "POST", body: { refresh },
|
||||
})
|
||||
if (request === suggestionRequest && messageId === props.messageId) suggestions.value = response.suggestions || []
|
||||
} catch (err: any) {
|
||||
if (request === suggestionRequest) suggestionError.value = err?.data?.error || "KI-Vorschläge konnten nicht geladen werden."
|
||||
} finally {
|
||||
if (request === suggestionRequest) loadingSuggestions.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.messageId, () => {
|
||||
suggestions.value = []
|
||||
links.value = [...(props.entityLinks || [])]
|
||||
loadSuggestions()
|
||||
}, { immediate: true })
|
||||
|
||||
const entityTypes = [
|
||||
{ label: "Kunde", value: "customers" },
|
||||
@@ -60,18 +93,22 @@ async function loadEntityOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
async function addLink() {
|
||||
if (!selectedEntityId.value) return
|
||||
async function addLink(suggestion?: Suggestion) {
|
||||
const entityId = suggestion?.entityId ?? selectedEntityId.value
|
||||
const entityType = suggestion?.entityType ?? selectedEntityType.value
|
||||
if (!entityId || saving.value) return
|
||||
const messageId = props.messageId
|
||||
|
||||
saving.value = true
|
||||
try {
|
||||
const response = await useNuxtApp().$api(`/api/email/messages/${props.messageId}/entity-links`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
entityType: selectedEntityType.value,
|
||||
entityId: selectedEntityId.value,
|
||||
entityType,
|
||||
entityId,
|
||||
},
|
||||
})
|
||||
if (!active || messageId !== props.messageId) return
|
||||
links.value = response.entityLinks || []
|
||||
emit("updated", links.value)
|
||||
selectedEntityId.value = null
|
||||
@@ -88,12 +125,14 @@ async function addLink() {
|
||||
}
|
||||
|
||||
async function removeLink(link: EntityLink) {
|
||||
const messageId = props.messageId
|
||||
saving.value = true
|
||||
try {
|
||||
const response = await useNuxtApp().$api(
|
||||
`/api/email/messages/${props.messageId}/entity-links/${link.entityType}/${link.entityId}`,
|
||||
{ method: "DELETE" },
|
||||
)
|
||||
if (!active || messageId !== props.messageId) return
|
||||
links.value = response.entityLinks || []
|
||||
emit("updated", links.value)
|
||||
toast.add({ title: "Verknüpfung entfernt", color: "success" })
|
||||
@@ -136,6 +175,35 @@ watch(selectedEntityType, loadEntityOptions, { immediate: true })
|
||||
<span v-if="!links.length" class="text-sm text-dimmed">Noch keine Zuordnung</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 border-t border-(--ui-border) pt-3" aria-live="polite">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="flex items-center gap-1.5 text-sm font-medium">
|
||||
<UIcon name="i-heroicons-sparkles" class="size-4" /> KI-Zuordnungsvorschläge
|
||||
</span>
|
||||
<UButton size="xs" variant="ghost" color="neutral" :disabled="loadingSuggestions || saving" @click="loadSuggestions(true)">
|
||||
Erneut prüfen
|
||||
</UButton>
|
||||
</div>
|
||||
<p v-if="loadingSuggestions" class="mt-2 text-sm text-dimmed">Die KI prüft passende Zuordnungen …</p>
|
||||
<p v-else-if="suggestionError" class="mt-2 text-sm text-error">{{ suggestionError }}</p>
|
||||
<template v-else>
|
||||
<p v-if="!visibleSuggestions.length" class="mt-2 text-sm text-dimmed">Keine weiteren passenden Zuordnungen gefunden.</p>
|
||||
<div v-for="suggestion in visibleSuggestions" :key="`${suggestion.entityType}:${suggestion.entityId}`"
|
||||
class="mt-2 flex items-start justify-between gap-3 rounded-md bg-(--ui-bg) p-2.5">
|
||||
<div class="min-w-0 text-sm">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="font-medium">{{ suggestion.entityTypeLabel }}: {{ suggestion.entityName }}</span>
|
||||
<UBadge size="xs" variant="soft" :color="suggestion.confidence === 'high' ? 'success' : 'warning'">
|
||||
{{ suggestion.confidence === 'high' ? 'Hohe Übereinstimmung' : 'Mögliche Zuordnung' }}
|
||||
</UBadge>
|
||||
</div>
|
||||
<p class="mt-1 text-dimmed">{{ suggestion.reason }}</p>
|
||||
</div>
|
||||
<UButton size="xs" icon="i-heroicons-link" :disabled="saving" @click="addLink(suggestion)">Übernehmen</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||||
<USelectMenu
|
||||
v-model="selectedEntityType"
|
||||
@@ -160,7 +228,7 @@ watch(selectedEntityType, loadEntityOptions, { immediate: true })
|
||||
size="sm"
|
||||
:loading="saving"
|
||||
:disabled="!selectedEntityId"
|
||||
@click="addLink"
|
||||
@click="addLink()"
|
||||
>
|
||||
Verknüpfen
|
||||
</UButton>
|
||||
|
||||
@@ -978,6 +978,7 @@ onMounted(loadAccounts)
|
||||
</div>
|
||||
|
||||
<EmailEntityLinks
|
||||
:key="selectedMessage.id"
|
||||
:message-id="selectedMessage.id"
|
||||
:entity-links="selectedMessage.entityLinks"
|
||||
@updated="updateSelectedMessageEntityLinks"
|
||||
|
||||
Reference in New Issue
Block a user