Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 66 additions & 3 deletions src/ui/src/builder/BuilderApp.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
/>
<div
class="mainGrid"
:class="{ openPanels: ssbm.openPanels.value.size > 0 }"
:class="{
openPanels: ssbm.openPanels.value.size > 0,
blocked: !canEditAgent,
}"
>
<BuilderHeader class="builderHeader" />
<BuilderSidebar
Expand Down Expand Up @@ -85,10 +88,11 @@
</template>
</ShareResizeVertical>
</div>
<BuilderEditBlockedState v-if="!canEditAgent" />

<!-- INSTANCE TRACKERS -->

<template v-if="builderMode !== 'preview'">
<template v-if="canEditAgent && builderMode !== 'preview'">
<BuilderCollaborationTracker
class="collaborationTracker"
></BuilderCollaborationTracker>
Expand Down Expand Up @@ -121,7 +125,7 @@

<!-- NOTES -->

<template v-if="builderMode === 'ui'">
<template v-if="canEditAgent && builderMode === 'ui'">
<BuilderInstanceTracker
v-for="note of notes"
:key="note.id"
Expand All @@ -145,6 +149,9 @@
<div id="drawer"></div>

<BuilderAppSocketTimeoutModal />
<BuilderRemigrationWarningDialog
v-model:is-open="showRemigrationWarningDialog"
/>
<!-- TOOLTIP -->

<BuilderTooltip id="tooltip" />
Expand Down Expand Up @@ -185,9 +192,12 @@ import BuilderAppSocketTimeoutModal from "./BuilderAppSocketTimeoutModal.vue";
import { useSocketTimeout } from "./useSocketTimeout";
import BlueprintsNavigationStack from "@/components/blueprints/BlueprintsNavigationStack.vue";
import BuilderDeprecationBanner from "./BuilderDeprecationBanner.vue";
import BuilderEditBlockedState from "./BuilderEditBlockedState.vue";
import BuilderRemigrationWarningDialog from "./BuilderRemigrationWarningDialog.vue";

const DEPRECATION_BANNER_DISMISSED_KEY =
"customAgentDeprecationBannerDismissed";
const REMIGRATION_WARNING_QUERY_PARAM = "showRemigrationWarning";

provide(injectionKeys.isAutogenModalShown, ref(false));

Expand Down Expand Up @@ -227,18 +237,57 @@ const isPostCutoff = computed(() =>
wf.featureFlags.value.includes("afterDeprecationCutoffAbv2"),
);
const isOrganizationAdmin = computed(() => wf.isOrganizationAdmin.value);
const canEditAgent = computed(() => wf.canEditAgent.value);
const showDeprecationBanner = computed(
() =>
wf.isWriterCloudApp.value &&
(isPostCutoff.value ||
(isPreCutoffFlagEnabled.value && !dismissed.value)),
);
const showRemigrationWarningDialog = ref(false);

function onDismissBanner() {
localStorage.setItem(DEPRECATION_BANNER_DISMISSED_KEY, "true");
dismissed.value = true;
}

function isPageRefresh() {
const navigation = performance.getEntriesByType("navigation")[0] as
| PerformanceNavigationTiming
| undefined;

return navigation?.type === "reload";
}

function consumeRemigrationWarningQueryParam() {
const url = new URL(window.location.href);
const shouldShowFromOpener =
url.searchParams.get(REMIGRATION_WARNING_QUERY_PARAM) === "true";

if (shouldShowFromOpener) {
url.searchParams.delete(REMIGRATION_WARNING_QUERY_PARAM);
window.history.replaceState(
window.history.state,
document.title,
url.toString(),
);
}

return shouldShowFromOpener;
}

onMounted(() => {
const shouldShowFromOpener = consumeRemigrationWarningQueryParam();

showRemigrationWarningDialog.value =
shouldShowFromOpener &&
!isPageRefresh() &&
canEditAgent.value &&
wf.isWriterCloudApp.value &&
isPreCutoffFlagEnabled.value &&
!isPostCutoff.value;
});

const tracking = useWriterTracking(wf);
const toasts = useToasts();

Expand Down Expand Up @@ -310,6 +359,8 @@ const notes = computed(() =>
);

async function handleKeydown(ev: KeyboardEvent) {
if (!canEditAgent.value) return;

if (ev.key === "Escape") {
ssbm.setSelection(null);
ssbm.expandedEditorForComponent.value = null;
Expand Down Expand Up @@ -406,11 +457,13 @@ async function handleKeydown(ev: KeyboardEvent) {
}

function handleRendererDragover(ev: DragEvent) {
if (!canEditAgent.value) return;
if (builderMode.value === "preview") return;
assignInsertionCandidacy(ev);
}

function handleRendererDrop(ev: DragEvent) {
if (!canEditAgent.value) return;
if (builderMode.value === "preview") return;
ssbm.setSelection(null);
const dropInfo = dropComponent(ev);
Expand All @@ -425,6 +478,7 @@ function handleRendererDrop(ev: DragEvent) {
}

function handleRendererClick(ev: PointerEvent): void {
if (!canEditAgent.value) return;
if (builderMode.value === "preview") return;

const unselectableEl = (ev.target as HTMLElement).closest<HTMLElement>(
Expand Down Expand Up @@ -471,10 +525,12 @@ function handleRendererClick(ev: PointerEvent): void {
ssbm.handleSelectionFromEvent(ev, targetId, targetInstancePath, "click");
}
function handleRendererDblClick() {
if (!canEditAgent.value) return;
ssbm.isSettingsBarCollapsed.value = false;
}

const handleRendererDragStart = (ev: DragEvent) => {
if (!canEditAgent.value) return;
if (builderMode.value === "preview") return;

const targetEl = (ev.target as HTMLElement).closest<HTMLElement>(
Expand All @@ -499,6 +555,7 @@ const handleRendererDragStart = (ev: DragEvent) => {
};

function handleRendererDragEnd(ev: DragEvent) {
if (!canEditAgent.value) return;
ssbm.setSelection(null);
removeInsertionCandidacy(ev);
}
Expand Down Expand Up @@ -593,6 +650,12 @@ onUnmounted(() => {
display: grid;
}

.mainGrid.blocked {
pointer-events: none;
filter: grayscale(1);
opacity: 0.52;
}

.builderHeader {
grid-column: 1 / 3;
grid-row: 1;
Expand Down
50 changes: 50 additions & 0 deletions src/ui/src/builder/BuilderEditBlockedState.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<template>
<div class="BuilderEditBlockedState">
<div class="BuilderEditBlockedState__card">
<h2>You don't have permission to edit this agent</h2>
<p>
Ask an org admin or AI Studio full access user to update this
agent, or to grant you AI Studio builder access.
</p>
</div>
</div>
</template>

<style scoped>
.BuilderEditBlockedState {
position: absolute;
inset: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: center;
padding: 32px;
background: rgb(255 255 255 / 64%);
backdrop-filter: grayscale(1);
}

.BuilderEditBlockedState__card {
max-width: 520px;
padding: 32px;
border: 1px solid var(--builderSeparatorColor);
border-radius: 8px;
background: var(--wdsColorWhite);
box-shadow: 0px 3px 40px 0px rgba(172, 185, 220, 0.24);
text-align: center;
}

.BuilderEditBlockedState__card h2 {
margin: 0 0 12px;
color: var(--primaryTextColor);
font-size: 24px;
font-weight: 500;
line-height: 1.4;
}

.BuilderEditBlockedState__card p {
margin: 0;
color: var(--secondaryTextColor);
font-size: 16px;
line-height: 1.6;
}
</style>
42 changes: 42 additions & 0 deletions src/ui/src/builder/BuilderRemigrationWarningDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<template>
<WdsModal
v-if="isOpen"
title="Migrate again to update your app"
size="compact"
display-close-button
:actions="actions"
@close="closeDialog"
>
<p class="BuilderRemigrationWarningDialog__body">
Changes made here won't sync automatically. If you already migrated
to Agent Builder v2, please migrate again to update your app.
</p>
</WdsModal>
</template>

<script setup lang="ts">
import { computed } from "vue";
import WdsModal, { type ModalAction } from "@/wds/WdsModal.vue";

const isOpen = defineModel("isOpen", { type: Boolean, default: false });

const actions = computed<ModalAction[]>(() => [
{
desc: "I understand",
fn: closeDialog,
},
]);

function closeDialog() {
isOpen.value = false;
}
</script>

<style scoped>
.BuilderRemigrationWarningDialog__body {
margin: 0 0 4px;
color: var(--primaryTextColor);
font-size: 16px;
line-height: 1.6;
}
</style>
8 changes: 8 additions & 0 deletions src/ui/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export function generateCore() {
apiKey?: string;
baseUrl?: string;
isOrganizationAdmin?: boolean;
canEdit?: boolean;
}
| undefined
>();
Expand Down Expand Up @@ -147,6 +148,12 @@ export function generateCore() {
const isWriterCloudApp = computed(() =>
Boolean(writerAppId.value || writerOrgId.value),
);
const canEditAgent = computed(
() =>
!isWriterCloudApp.value ||
isOrganizationAdmin.value ||
Boolean(writerApplication.value?.canEdit),
);

/**
* Initialise the core.
Expand Down Expand Up @@ -1176,6 +1183,7 @@ export function generateCore() {
writerApiKey,
writerBaseUrl,
isOrganizationAdmin,
canEditAgent,
...navigationStack,
getFirstBlueprint,
getFirstPage,
Expand Down
17 changes: 16 additions & 1 deletion src/ui/src/tests/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ export function buildMockCore() {
const userFunctions = shallowRef<UserFunction[]>([]);
const featureFlags = shallowRef<string[]>([]);
const writerApplication = shallowRef<
{ id: string; organizationId: string } | undefined
| {
id: string;
organizationId: string;
isOrganizationAdmin?: boolean;
canEdit?: boolean;
}
| undefined
>();

core.userFunctions = userFunctions;
Expand All @@ -67,6 +73,15 @@ export function buildMockCore() {
() => Number(writerApplication.value?.organizationId) || undefined,
);
core.writerAppId = computed(() => writerApplication.value?.id);
core.isOrganizationAdmin = computed(
() => writerApplication.value?.isOrganizationAdmin ?? false,
);
core.canEditAgent = computed(
() =>
!core.isWriterCloudApp.value ||
core.isOrganizationAdmin.value ||
Boolean(writerApplication.value?.canEdit),
);

vi.spyOn(core, "sendComponentUpdate").mockImplementation(async () => {});

Expand Down
10 changes: 9 additions & 1 deletion src/ui/src/wds/WdsModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
class="WdsModal colorTransformer"
:class="{
'WdsModal--overflow': allowOverflow,
'WdsModal--compact': size == 'compact',
'WdsModal--wide': size == 'wide',
}"
tabindex="-1"
Expand Down Expand Up @@ -78,7 +79,7 @@ export type ModalAction = {
const props = defineProps({
title: { type: String, required: false, default: null },
size: {
type: String as PropType<"normal" | "wide">,
type: String as PropType<"compact" | "normal" | "wide">,
required: false,
default: "normal",
},
Expand Down Expand Up @@ -132,6 +133,13 @@ const { title, actions } = toRefs(props);
max-width: 240ch;
}

.WdsModal--compact .WdsModal__main {
width: min(560px, calc(100vw - 32px));
max-width: calc(100vw - 32px);
gap: 20px;
padding: 24px;
}

.WdsModal--overflow .WdsModal__main,
.WdsModal--overflow .WdsModal__main__content {
overflow: unset;
Expand Down
3 changes: 3 additions & 0 deletions src/writer/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,15 @@ def _handle_session_init(
writer_base_url = os.getenv("WRITER_BASE_URL", "https://api.writer.com")
is_org_admin_header = headers.get("x-is-org-admin", "").lower()
is_org_admin = is_org_admin_header == "true"
can_edit_header = headers.get("x-can-edit-agent", "").lower()
can_edit = is_org_admin or can_edit_header == "true"
if writer_app_id is not None and writer_org_id is not None:
writer_application = WriterApplicationInformation(
id=writer_app_id,
organizationId=writer_org_id,
baseUrl=writer_base_url,
isOrganizationAdmin=is_org_admin,
canEdit=can_edit,
)
if writer.Config.mode == "edit":
writer_application.apiKey = os.getenv("WRITER_API_KEY")
Expand Down
1 change: 1 addition & 0 deletions src/writer/ss_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class WriterApplicationInformation(BaseModel):
baseUrl: Optional[str] = None
apiKey: Optional[str] = None
isOrganizationAdmin: Optional[bool] = False
canEdit: Optional[bool] = False


class AutogenRequestBody(BaseModel):
Expand Down
Loading