forked from trent_larson/crowd-funder-for-time-pwa
toggle embeddings on list screen, distinguish between empty profile description and hidden profile
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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'"
|
||||
|
||||
Reference in New Issue
Block a user