add a toggle for generate-embeddings flags for admins, and label DID actions

This commit is contained in:
2026-01-31 17:59:52 -07:00
parent 8991b29705
commit cc7c7eb88b

View File

@@ -138,92 +138,110 @@
</div>
<div class="flex justify-between mt-4">
<div class="flex items-center">
<div v-if="activeDid" class="flex justify-between">
<div>
<button
v-if="
contactFromDid?.seesMe && contactFromDid.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="They can see your activity"
@click="confirmSetVisibility(contactFromDid, false)"
>
<font-awesome icon="arrow-up" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
</button>
<button
v-else-if="
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="They cannot see your activity"
@click="confirmSetVisibility(contactFromDid, true)"
>
<font-awesome icon="arrow-up" class="fa-fw" />
<font-awesome icon="eye-slash" class="fa-fw" />
</button>
<button
v-if="
contactFromDid?.iViewContent &&
contactFromDid.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="You watch their activity"
@click="confirmViewContent(contactFromDid, false)"
>
<font-awesome icon="arrow-down" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
</button>
<button
v-else-if="
!contactFromDid?.iViewContent &&
contactFromDid?.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="You do not watch their activity"
@click="confirmViewContent(contactFromDid, true)"
>
<font-awesome icon="arrow-down" class="fa-fw" />
<font-awesome icon="eye-slash" class="fa-fw" />
</button>
<button
<div v-if="activeDid" class="flex justify-between items-end">
<div class="flex items-end gap-1">
<div
v-if="contactFromDid?.did !== activeDid"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Check Visibility"
@click="checkVisibility(contactFromDid)"
class="flex flex-col items-center"
>
<font-awesome icon="rotate" class="fa-fw" />
</button>
<button
v-if="contactFromDid?.seesMe"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="They can see your activity"
@click="confirmSetVisibility(contactFromDid, false)"
>
<font-awesome icon="arrow-up" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
</button>
<button
v-else-if="!contactFromDid?.seesMe"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="They cannot see your activity"
@click="confirmSetVisibility(contactFromDid, true)"
>
<font-awesome icon="arrow-up" class="fa-fw" />
<font-awesome icon="eye-slash" class="fa-fw" />
</button>
<span class="text-xs text-slate-600">See You</span>
</div>
<div
v-if="contactFromDid?.did !== activeDid"
class="flex flex-col items-center"
>
<button
v-if="contactFromDid?.iViewContent"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="You watch their activity"
@click="confirmViewContent(contactFromDid, false)"
>
<font-awesome icon="arrow-down" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
</button>
<button
v-else-if="!contactFromDid?.iViewContent"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="You do not watch their activity"
@click="confirmViewContent(contactFromDid, true)"
>
<font-awesome icon="arrow-down" class="fa-fw" />
<font-awesome icon="eye-slash" class="fa-fw" />
</button>
<span class="text-xs text-slate-600">Watch Them</span>
</div>
<div
v-if="contactFromDid?.did !== activeDid"
class="flex flex-col items-center"
>
<button
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Check Visibility"
@click="checkVisibility(contactFromDid)"
>
<font-awesome icon="rotate" class="fa-fw" />
</button>
<span class="text-xs text-slate-600">Check</span>
</div>
</div>
<button
<div
v-if="contactFromDid?.did !== activeDid"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Registration"
@click="confirmRegister(contactFromDid)"
class="flex flex-col items-center ml-6"
>
<font-awesome
v-if="contactFromDid?.registered"
icon="person-circle-check"
class="fa-fw"
/>
<font-awesome
v-else
icon="person-circle-question"
class="fa-fw"
/>
</button>
<button
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Registration"
@click="confirmRegister(contactFromDid)"
>
<font-awesome
v-if="contactFromDid?.registered"
icon="person-circle-check"
class="fa-fw"
/>
<font-awesome
v-else
icon="person-circle-question"
class="fa-fw"
/>
</button>
<span class="text-xs text-slate-600">Register</span>
</div>
</div>
<button
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Delete"
@click="confirmDeleteContact(contactFromDid)"
<div
v-if="contactFromDid?.did !== activeDid"
class="flex flex-col items-center ml-6"
>
<font-awesome icon="trash-can" class="fa-fw" />
</button>
<button
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Delete"
@click="confirmDeleteContact(contactFromDid)"
>
<font-awesome icon="trash-can" class="fa-fw" />
</button>
<span class="text-xs text-slate-600">Delete</span>
</div>
</div>
<div v-if="!contactFromDid?.profileImageUrl">
<div>Auto-Generated Icon</div>
@@ -263,6 +281,40 @@
</div>
</div>
<!-- Only admins can set the generate-embedding flag -->
<div
v-if="showGeneralAdvanced && viewingDid"
class="mt-4 pt-4 border-t border-slate-300"
data-testid="generateEmbeddingSection"
>
<label class="block text-sm font-medium text-gray-700 mb-2">
Always generate embedding
</label>
<div class="flex items-center gap-2">
<button
type="button"
role="switch"
:aria-checked="generateEmbedding"
:disabled="generateEmbeddingSaving || generateEmbeddingLoading"
class="relative inline-flex h-6 w-11 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-2 disabled:opacity-50 disabled:cursor-not-allowed"
:class="generateEmbedding ? 'bg-blue-600' : 'bg-gray-200'"
@click="toggleGenerateEmbedding"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition"
:class="generateEmbedding ? 'translate-x-5' : 'translate-x-1'"
/>
</button>
<span class="text-sm text-gray-600">
{{ generateEmbedding ? "On" : "Off" }}
<span v-if="generateEmbeddingLoading" class="ml-1">(loading)</span>
<span v-else-if="generateEmbeddingSaving" class="ml-1"
>(saving)</span
>
</span>
</div>
</div>
<!-- Loading Animation -->
<div
v-if="isLoading"
@@ -332,7 +384,10 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import InfiniteScroll from "../components/InfiniteScroll.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import {
DEFAULT_PARTNER_API_SERVER,
NotificationIface,
} from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { BoundingBox } from "../db/tables/settings";
@@ -406,11 +461,16 @@ export default class DIDView extends Vue {
contactLabels: string[] = [];
contactYaml = "";
generateEmbedding: boolean | null = null;
generateEmbeddingLoading = false;
generateEmbeddingSaving = false;
hitEnd = false;
isLoading = false;
isMyDid = false;
partnerApiServer = DEFAULT_PARTNER_API_SERVER;
searchBox: { name: string; bbox: BoundingBox } | null = null;
showDidDetails = false;
showGeneralAdvanced = false;
showLargeIdenticonId?: string;
showLargeIdenticonUrl?: string;
viewingDid?: string;
@@ -444,6 +504,9 @@ export default class DIDView extends Vue {
await this.loadContactInformation();
await this.loadClaimsAbout();
await this.checkIfOwnDID();
if (this.showGeneralAdvanced && this.activeDid) {
await this.loadGenerateEmbeddingState();
}
}
}
@@ -459,6 +522,9 @@ export default class DIDView extends Vue {
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.partnerApiServer =
(settings.partnerApiServer as string) || DEFAULT_PARTNER_API_SERVER;
}
/**
@@ -519,6 +585,71 @@ export default class DIDView extends Vue {
this.isMyDid = allAccountDids.includes(this.viewingDid);
}
/**
* Loads partner profile generateEmbedding state for the viewed DID (when showGeneralAdvanced).
*/
private async loadGenerateEmbeddingState() {
if (!this.viewingDid || !this.activeDid) return;
this.generateEmbeddingLoading = true;
try {
const headers = await getHeaders(this.activeDid);
const url = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${encodeURIComponent(this.viewingDid)}`;
const response = await this.axios.get(url, { headers });
const data = response.data?.data;
this.generateEmbedding =
data && typeof data.generateEmbedding === "boolean"
? data.generateEmbedding
: false;
} catch {
this.generateEmbedding = false;
} finally {
this.generateEmbeddingLoading = false;
}
}
/**
* Toggles the "always generate embedding" flag for the viewed DID on the partner API.
* Only permissioned (admin) users can change this; API returns 403 otherwise.
*/
async toggleGenerateEmbedding() {
if (
!this.viewingDid ||
!this.activeDid ||
this.generateEmbeddingSaving ||
this.generateEmbeddingLoading
) {
return;
}
const newValue = !this.generateEmbedding;
this.generateEmbeddingSaving = true;
try {
const headers = await getHeaders(this.activeDid);
const url = `${this.partnerApiServer}/api/partner/userProfile/${encodeURIComponent(this.viewingDid)}/generateEmbedding`;
await this.axios.put(url, { generateEmbedding: newValue }, { headers });
this.generateEmbedding = newValue;
this.notify.success(
newValue
? "Contact tagged to always generate embedding."
: "Contact untagged from always generating embedding.",
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 generate-embedding flag. Try again.",
TIMEOUTS.LONG,
);
}
} finally {
this.generateEmbeddingSaving = false;
}
}
/**
* Loads additional claims when user scrolls to bottom
* Used by infinite scroll component to implement pagination