toggle embeddings on list screen, distinguish between empty profile description and hidden profile

This commit is contained in:
2026-02-22 15:40:21 -07:00
parent 41e50bdf95
commit e7e2830807
2 changed files with 291 additions and 42 deletions

View File

@@ -37,6 +37,59 @@
/>
</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">
<li
v-for="result in contactResults"
@@ -57,9 +110,28 @@
</span>
<span
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
v-else-if="result.status === 'no-profile'"
@@ -95,11 +167,17 @@
>
Has profile
</span>
<span
v-else-if="result.status === 'blank-profile'"
class="text-amber-600"
>
Has profile with blank description
</span>
<span
v-else-if="result.status === 'no-profile'"
class="text-amber-600"
>
No profile
No profile or not visible
</span>
<span
v-else-if="result.status === 'error'"
@@ -117,6 +195,49 @@
>
{{ result.profileDescription }}
</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>
<router-link
@@ -128,25 +249,6 @@
</div>
</li>
</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>
</section>
</template>
@@ -166,11 +268,23 @@ import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
interface EmbeddingMetadata {
generateEmbedding: boolean;
isForEmptyString: boolean;
}
interface ContactCheckResult {
did: string;
name: string;
status: "pending" | "checking" | "has-profile" | "no-profile" | "error";
status:
| "pending"
| "checking"
| "has-profile"
| "blank-profile"
| "no-profile"
| "error";
profileDescription?: string;
embeddingMetadata?: EmbeddingMetadata | null;
error?: string;
}
@@ -186,12 +300,13 @@ export default class ContactProfileCheckView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>;
activeDid = "";
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
contactResults: ContactCheckResult[] = [];
embeddingSavingDids: Set<string> = new Set();
isRefreshing = false;
created() {
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;
}
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;
}
@@ -279,6 +399,24 @@ export default class ContactProfileCheckView extends Vue {
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() {
if (!this.activeDid) {
this.notify.error("No active identity.", TIMEOUTS.LONG);
@@ -300,6 +438,8 @@ export default class ContactProfileCheckView extends Vue {
if (data && data.description) {
this.contactResults[i].status = "has-profile";
this.contactResults[i].profileDescription = data.description;
} else if (data && typeof data.description === "string") {
this.contactResults[i].status = "blank-profile";
} else {
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];
}
@@ -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() {
this.$router.back();
}

View File

@@ -127,23 +127,6 @@
</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 -->
<ul
v-if="contacts.length > 0"
@@ -184,6 +167,23 @@
@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
ref="customGivenDialog"
:initial-giver-entity-type="'person'"