toggle embeddings on list screen, distinguish between empty profile description and hidden profile
This commit is contained in:
@@ -37,6 +37,59 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isComplete" class="mb-6 p-4 bg-slate-100 rounded-lg">
|
||||||
|
<div class="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<div class="font-semibold text-slate-800">
|
||||||
|
<span class="text-green-600">{{ fullyReadyCount }}</span>
|
||||||
|
of {{ contactResults.length }}
|
||||||
|
{{ contactResults.length === 1 ? "contact" : "contacts" }}
|
||||||
|
fully ready
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
v-if="fullyReadyCount < contactResults.length"
|
||||||
|
class="mt-2 text-sm text-slate-600 list-disc list-inside space-y-0.5"
|
||||||
|
>
|
||||||
|
<li v-if="blankProfileCount > 0">
|
||||||
|
<span class="text-amber-600 font-medium">{{
|
||||||
|
blankProfileCount
|
||||||
|
}}</span>
|
||||||
|
{{ blankProfileCount === 1 ? "profile" : "profiles" }} with
|
||||||
|
blank description
|
||||||
|
</li>
|
||||||
|
<li v-if="noProfileOrHiddenCount > 0">
|
||||||
|
<span class="text-amber-600 font-medium">{{
|
||||||
|
noProfileOrHiddenCount
|
||||||
|
}}</span>
|
||||||
|
no profile or not visible
|
||||||
|
</li>
|
||||||
|
<li v-if="noEmbeddingOrHiddenCount > 0">
|
||||||
|
<span class="text-amber-600 font-medium">{{
|
||||||
|
noEmbeddingOrHiddenCount
|
||||||
|
}}</span>
|
||||||
|
no embedding or not visible
|
||||||
|
</li>
|
||||||
|
<li v-if="errorCount > 0">
|
||||||
|
<span class="text-red-600 font-medium">{{ errorCount }}</span>
|
||||||
|
{{ errorCount === 1 ? "check" : "checks" }} failed
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="flex-shrink-0 text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
:disabled="isRefreshing"
|
||||||
|
@click="refreshChecks"
|
||||||
|
>
|
||||||
|
<font-awesome
|
||||||
|
icon="rotate"
|
||||||
|
class="fa-fw mr-1"
|
||||||
|
:class="{ 'fa-spin': isRefreshing }"
|
||||||
|
/>
|
||||||
|
{{ isRefreshing ? "Refreshing…" : "Refresh" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
<li
|
<li
|
||||||
v-for="result in contactResults"
|
v-for="result in contactResults"
|
||||||
@@ -57,9 +110,28 @@
|
|||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="result.status === 'has-profile'"
|
v-else-if="result.status === 'has-profile'"
|
||||||
class="text-green-600"
|
:class="
|
||||||
|
result.embeddingMetadata?.generateEmbedding &&
|
||||||
|
!result.embeddingMetadata?.isForEmptyString
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-amber-500'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<font-awesome icon="circle-check" class="fa-fw" />
|
<font-awesome
|
||||||
|
:icon="
|
||||||
|
result.embeddingMetadata?.generateEmbedding &&
|
||||||
|
!result.embeddingMetadata?.isForEmptyString
|
||||||
|
? 'circle-check'
|
||||||
|
: 'circle-xmark'
|
||||||
|
"
|
||||||
|
class="fa-fw"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="result.status === 'blank-profile'"
|
||||||
|
class="text-amber-500"
|
||||||
|
>
|
||||||
|
<font-awesome icon="circle-xmark" class="fa-fw" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="result.status === 'no-profile'"
|
v-else-if="result.status === 'no-profile'"
|
||||||
@@ -95,11 +167,17 @@
|
|||||||
>
|
>
|
||||||
Has profile
|
Has profile
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="result.status === 'blank-profile'"
|
||||||
|
class="text-amber-600"
|
||||||
|
>
|
||||||
|
Has profile with blank description
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="result.status === 'no-profile'"
|
v-else-if="result.status === 'no-profile'"
|
||||||
class="text-amber-600"
|
class="text-amber-600"
|
||||||
>
|
>
|
||||||
No profile
|
No profile or not visible
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else-if="result.status === 'error'"
|
v-else-if="result.status === 'error'"
|
||||||
@@ -117,6 +195,49 @@
|
|||||||
>
|
>
|
||||||
{{ result.profileDescription }}
|
{{ result.profileDescription }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="
|
||||||
|
result.status !== 'pending' && result.status !== 'checking'
|
||||||
|
"
|
||||||
|
class="mt-2 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="
|
||||||
|
result.embeddingMetadata?.generateEmbedding ?? false
|
||||||
|
"
|
||||||
|
:disabled="embeddingSavingDids.has(result.did)"
|
||||||
|
class="relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
:class="
|
||||||
|
(result.embeddingMetadata?.generateEmbedding ?? false)
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
|
@click="toggleEmbedding(result.did)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition"
|
||||||
|
:class="
|
||||||
|
(result.embeddingMetadata?.generateEmbedding ?? false)
|
||||||
|
? 'translate-x-4'
|
||||||
|
: 'translate-x-0'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<span class="text-xs text-gray-600">
|
||||||
|
Embedding:
|
||||||
|
{{
|
||||||
|
(result.embeddingMetadata?.generateEmbedding ?? false)
|
||||||
|
? "On"
|
||||||
|
: "Off"
|
||||||
|
}}
|
||||||
|
<span v-if="embeddingSavingDids.has(result.did)" class="ml-1"
|
||||||
|
>(saving…)</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
@@ -128,25 +249,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div v-if="isComplete" class="mt-6 p-4 bg-slate-100 rounded-lg">
|
|
||||||
<h3 class="font-semibold text-slate-800 mb-2">Summary</h3>
|
|
||||||
<div class="text-sm text-slate-600 space-y-1">
|
|
||||||
<div>
|
|
||||||
<span class="text-green-600 font-medium">{{ profileCount }}</span>
|
|
||||||
{{ profileCount === 1 ? "contact has" : "contacts have" }} a profile
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span class="text-amber-600 font-medium">{{ noProfileCount }}</span>
|
|
||||||
{{ noProfileCount === 1 ? "contact has" : "contacts have" }} no
|
|
||||||
profile or it's not visible to you
|
|
||||||
</div>
|
|
||||||
<div v-if="errorCount > 0">
|
|
||||||
<span class="text-red-600 font-medium">{{ errorCount }}</span>
|
|
||||||
{{ errorCount === 1 ? "check" : "checks" }} failed
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -166,11 +268,23 @@ import { logger } from "../utils/logger";
|
|||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
|
||||||
|
interface EmbeddingMetadata {
|
||||||
|
generateEmbedding: boolean;
|
||||||
|
isForEmptyString: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface ContactCheckResult {
|
interface ContactCheckResult {
|
||||||
did: string;
|
did: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: "pending" | "checking" | "has-profile" | "no-profile" | "error";
|
status:
|
||||||
|
| "pending"
|
||||||
|
| "checking"
|
||||||
|
| "has-profile"
|
||||||
|
| "blank-profile"
|
||||||
|
| "no-profile"
|
||||||
|
| "error";
|
||||||
profileDescription?: string;
|
profileDescription?: string;
|
||||||
|
embeddingMetadata?: EmbeddingMetadata | null;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,12 +300,13 @@ export default class ContactProfileCheckView extends Vue {
|
|||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
$route!: RouteLocationNormalizedLoaded;
|
$route!: RouteLocationNormalizedLoaded;
|
||||||
$router!: Router;
|
$router!: Router;
|
||||||
|
|
||||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
notify!: ReturnType<typeof createNotifyHelpers>;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
|
||||||
contactResults: ContactCheckResult[] = [];
|
contactResults: ContactCheckResult[] = [];
|
||||||
|
embeddingSavingDids: Set<string> = new Set();
|
||||||
|
isRefreshing = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.notify = createNotifyHelpers(this.$notify);
|
this.notify = createNotifyHelpers(this.$notify);
|
||||||
@@ -271,7 +386,12 @@ export default class ContactProfileCheckView extends Vue {
|
|||||||
return this.contactResults.filter((r) => r.status === "has-profile").length;
|
return this.contactResults.filter((r) => r.status === "has-profile").length;
|
||||||
}
|
}
|
||||||
|
|
||||||
get noProfileCount(): number {
|
get blankProfileCount(): number {
|
||||||
|
return this.contactResults.filter((r) => r.status === "blank-profile")
|
||||||
|
.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get noProfileOrHiddenCount(): number {
|
||||||
return this.contactResults.filter((r) => r.status === "no-profile").length;
|
return this.contactResults.filter((r) => r.status === "no-profile").length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,6 +399,24 @@ export default class ContactProfileCheckView extends Vue {
|
|||||||
return this.contactResults.filter((r) => r.status === "error").length;
|
return this.contactResults.filter((r) => r.status === "error").length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get noEmbeddingOrHiddenCount(): number {
|
||||||
|
return this.contactResults.filter(
|
||||||
|
(r) =>
|
||||||
|
r.status !== "pending" &&
|
||||||
|
r.status !== "checking" &&
|
||||||
|
!r.embeddingMetadata?.generateEmbedding,
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fullyReadyCount(): number {
|
||||||
|
return this.contactResults.filter(
|
||||||
|
(r) =>
|
||||||
|
r.status === "has-profile" &&
|
||||||
|
r.embeddingMetadata?.generateEmbedding &&
|
||||||
|
!r.embeddingMetadata?.isForEmptyString,
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
private async runChecks() {
|
private async runChecks() {
|
||||||
if (!this.activeDid) {
|
if (!this.activeDid) {
|
||||||
this.notify.error("No active identity.", TIMEOUTS.LONG);
|
this.notify.error("No active identity.", TIMEOUTS.LONG);
|
||||||
@@ -300,6 +438,8 @@ export default class ContactProfileCheckView extends Vue {
|
|||||||
if (data && data.description) {
|
if (data && data.description) {
|
||||||
this.contactResults[i].status = "has-profile";
|
this.contactResults[i].status = "has-profile";
|
||||||
this.contactResults[i].profileDescription = data.description;
|
this.contactResults[i].profileDescription = data.description;
|
||||||
|
} else if (data && typeof data.description === "string") {
|
||||||
|
this.contactResults[i].status = "blank-profile";
|
||||||
} else {
|
} else {
|
||||||
this.contactResults[i].status = "no-profile";
|
this.contactResults[i].status = "no-profile";
|
||||||
}
|
}
|
||||||
@@ -320,6 +460,32 @@ export default class ContactProfileCheckView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
const embUrl =
|
||||||
|
`${this.partnerApiServer}/api/partner/userProfileEmbeddingMetadata/` +
|
||||||
|
encodeURIComponent(this.contactResults[i].did);
|
||||||
|
const embResponse = await this.axios.get(embUrl, { headers });
|
||||||
|
const embData = embResponse.data?.data;
|
||||||
|
if (embData && typeof embData.generateEmbedding === "boolean") {
|
||||||
|
this.contactResults[i].embeddingMetadata = {
|
||||||
|
generateEmbedding: embData.generateEmbedding,
|
||||||
|
isForEmptyString: !!embData.isForEmptyString,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.contactResults[i].embeddingMetadata = null;
|
||||||
|
}
|
||||||
|
} catch (embErr: unknown) {
|
||||||
|
const axiosErr = embErr as { response?: { status?: number } };
|
||||||
|
if (axiosErr.response?.status !== 404) {
|
||||||
|
logger.error(
|
||||||
|
`Failed to load embedding metadata for ${this.contactResults[i].did}:`,
|
||||||
|
embErr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.contactResults[i].embeddingMetadata = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.contactResults = [...this.contactResults];
|
this.contactResults = [...this.contactResults];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,6 +499,89 @@ export default class ContactProfileCheckView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshChecks() {
|
||||||
|
this.isRefreshing = true;
|
||||||
|
for (let i = 0; i < this.contactResults.length; i++) {
|
||||||
|
this.contactResults[i].status = "pending";
|
||||||
|
this.contactResults[i].profileDescription = undefined;
|
||||||
|
this.contactResults[i].embeddingMetadata = undefined;
|
||||||
|
this.contactResults[i].error = undefined;
|
||||||
|
}
|
||||||
|
this.contactResults = [...this.contactResults];
|
||||||
|
await this.runChecks();
|
||||||
|
this.isRefreshing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleEmbedding(did: string) {
|
||||||
|
const idx = this.contactResults.findIndex((r) => r.did === did);
|
||||||
|
if (idx === -1 || !this.activeDid) return;
|
||||||
|
|
||||||
|
const current =
|
||||||
|
this.contactResults[idx].embeddingMetadata?.generateEmbedding ?? false;
|
||||||
|
const newValue = !current;
|
||||||
|
|
||||||
|
this.embeddingSavingDids = new Set(this.embeddingSavingDids).add(did);
|
||||||
|
try {
|
||||||
|
const headers = await getHeaders(this.activeDid);
|
||||||
|
const url =
|
||||||
|
`${this.partnerApiServer}/api/partner/userProfileGenerateEmbedding/` +
|
||||||
|
encodeURIComponent(did);
|
||||||
|
await this.axios.put(url, { generateEmbedding: newValue }, { headers });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const embUrl =
|
||||||
|
`${this.partnerApiServer}/api/partner/userProfileEmbeddingMetadata/` +
|
||||||
|
encodeURIComponent(did);
|
||||||
|
const embResponse = await this.axios.get(embUrl, { headers });
|
||||||
|
const embData = embResponse.data?.data;
|
||||||
|
if (embData && typeof embData.generateEmbedding === "boolean") {
|
||||||
|
this.contactResults[idx].embeddingMetadata = {
|
||||||
|
generateEmbedding: embData.generateEmbedding,
|
||||||
|
isForEmptyString: !!embData.isForEmptyString,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (refreshErr) {
|
||||||
|
logger.error(
|
||||||
|
"Failed to refresh embedding metadata",
|
||||||
|
!newValue
|
||||||
|
? "- where a 404 is expected when embedding is turned off:"
|
||||||
|
: ":",
|
||||||
|
refreshErr,
|
||||||
|
);
|
||||||
|
this.contactResults[idx].embeddingMetadata = {
|
||||||
|
generateEmbedding: newValue,
|
||||||
|
isForEmptyString:
|
||||||
|
this.contactResults[idx].embeddingMetadata?.isForEmptyString ??
|
||||||
|
true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.contactResults = [...this.contactResults];
|
||||||
|
|
||||||
|
this.notify.success(
|
||||||
|
newValue
|
||||||
|
? "Embedding generation enabled."
|
||||||
|
: "Embedding generation disabled.",
|
||||||
|
TIMEOUTS.STANDARD,
|
||||||
|
);
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = (err as { response?: { data?: { error?: string } } })
|
||||||
|
?.response?.data?.error;
|
||||||
|
if (error) {
|
||||||
|
this.notify.error(error, TIMEOUTS.LONG);
|
||||||
|
} else {
|
||||||
|
logger.error("Failed to update generate-embedding flag:", err);
|
||||||
|
this.notify.error(
|
||||||
|
"Failed to update embedding flag. Try again.",
|
||||||
|
TIMEOUTS.LONG,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const updated = new Set(this.embeddingSavingDids);
|
||||||
|
updated.delete(did);
|
||||||
|
this.embeddingSavingDids = updated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
goBack() {
|
goBack() {
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,23 +127,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Check for Profile (admin/organizer feature) -->
|
|
||||||
<div
|
|
||||||
v-if="showGeneralAdvanced && contactsSelected.length > 0"
|
|
||||||
class="my-3 flex justify-center"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="text-sm bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
|
||||||
@click="checkSelectedProfiles"
|
|
||||||
>
|
|
||||||
<font-awesome icon="circle-user" class="fa-fw mr-1" />
|
|
||||||
Check for Public Profile
|
|
||||||
</button>
|
|
||||||
<button class="ml-4 text-2xl text-blue-500" @click="showProfileCheckInfo">
|
|
||||||
<font-awesome icon="circle-info" class="fa-fw" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Results List -->
|
<!-- Results List -->
|
||||||
<ul
|
<ul
|
||||||
v-if="contacts.length > 0"
|
v-if="contacts.length > 0"
|
||||||
@@ -184,6 +167,23 @@
|
|||||||
@copy-selected="copySelectedContacts"
|
@copy-selected="copySelectedContacts"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Check for Profile (admin/organizer feature) -->
|
||||||
|
<div
|
||||||
|
v-if="showGeneralAdvanced && contactsSelected.length > 0"
|
||||||
|
class="my-3 flex justify-center"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="text-sm bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
|
@click="checkSelectedProfiles"
|
||||||
|
>
|
||||||
|
<font-awesome icon="circle-user" class="fa-fw mr-1" />
|
||||||
|
Check for Public Profile
|
||||||
|
</button>
|
||||||
|
<button class="ml-4 text-2xl text-blue-500" @click="showProfileCheckInfo">
|
||||||
|
<font-awesome icon="circle-info" class="fa-fw" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<GiftedDialog
|
<GiftedDialog
|
||||||
ref="customGivenDialog"
|
ref="customGivenDialog"
|
||||||
:initial-giver-entity-type="'person'"
|
:initial-giver-entity-type="'person'"
|
||||||
|
|||||||
Reference in New Issue
Block a user