Skip to content
Open
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
77 changes: 77 additions & 0 deletions src/ui/src/builder/BuilderHeaderMoreDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@
v-model="importConfirmCheckbox"
label="Yes, I want to replace the current agent with the imported file"
/>
<div
v-if="importErrorSummary"
class="BuilderHeaderMoreDropdown__importError"
>
<p class="BuilderHeaderMoreDropdown__importError__header">
Failed to import the agent
</p>

<p class="BuilderHeaderMoreDropdown__importError__summary">
{{ importErrorSummary }}
</p>

<details
v-if="importErrorDetails"
class="BuilderHeaderMoreDropdown__importError__details"
>
<summary>Show details</summary>
<pre class="BuilderHeaderMoreDropdown__importError__trace">{{
importErrorDetails
}}</pre>
</details>
</div>
</WdsModal>
</template>

Expand Down Expand Up @@ -60,6 +82,8 @@ const toasts = useToasts();
const importInProgress = ref(false);
const importConfirmShown = ref(false);
const importConfirmCheckbox = ref(false);
const importErrorSummary = ref("");
const importErrorDetails = ref("");

const importModalActions = computed<ModalAction[]>(() => [
{
Expand All @@ -68,6 +92,8 @@ const importModalActions = computed<ModalAction[]>(() => [
fn: () => {
importConfirmCheckbox.value = false;
importConfirmShown.value = false;
importErrorSummary.value = "";
importErrorDetails.value = "";
},
},
{
Expand Down Expand Up @@ -105,6 +131,8 @@ async function importModalConfirm() {
if (!file) return;

importInProgress.value = true;
importErrorSummary.value = "";
importErrorDetails.value = "";

try {
// Prepare form data
Expand All @@ -117,6 +145,9 @@ async function importModalConfirm() {
body: formData,
});
if (!response.ok) {
const parsed = await response.json();
importErrorSummary.value = parsed?.detail?.summary;
importErrorDetails.value = parsed?.detail?.details;
throw new Error("Failed to connect to import API");
}
importConfirmShown.value = false;
Expand Down Expand Up @@ -162,4 +193,50 @@ async function onSelect(key: string) {
.BuilderHeaderMoreDropdown:deep(.SharedMoreDropdown__dropdown) {
min-width: 220px;
}

.BuilderHeaderMoreDropdown__importError {
margin-top: 16px;
padding: 12px 14px;
border-radius: 8px;
background: rgba(255, 149, 0, 0.08);
border: 1px solid rgba(255, 149, 0, 0.35);
grid-column: 2 / 4;
}

.BuilderHeaderMoreDropdown__importError__header {
font-weight: 600;
color: var(--wdsColorOrange4);
font-size: 14px;
}

.BuilderHeaderMoreDropdown__importError__summary {
margin-top: 6px;
font-size: 14px;
}

.BuilderHeaderMoreDropdown__importError__details {
margin-top: 10px;
}

.BuilderHeaderMoreDropdown__importError__details summary {
cursor: pointer;
font-size: 12px;
user-select: none;
}

.BuilderHeaderMoreDropdown__importError__details summary:hover {
text-decoration: underline;
}

.BuilderHeaderMoreDropdown__importError__trace {
margin-top: 8px;
max-height: 220px;
overflow: auto;
padding: 10px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.05);
font-size: 12px;
line-height: 1.4;
white-space: pre;
}
</style>
3 changes: 3 additions & 0 deletions src/writer/app_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,9 @@ async def import_zip(self, zip_path: str):
if not os.path.isdir(wf_dir_path):
raise ValueError(".wf directory not found alongside main.py in the archive.")

# Parse files to ensure there are no errors
wf_project.read_files(main_py_dir)

# Passed all checks; replace current app contents

logging.info("Copying app at %s", main_py_dir)
Expand Down
10 changes: 5 additions & 5 deletions src/writer/serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,9 @@ async def export_zip():
@app.post("/api/import")
async def import_zip(file: UploadFile = File(...)):
if serve_mode != "edit":
raise HTTPException(status_code=403, detail="Invalid mode.")
raise HTTPException(status_code=403, detail={"summary": "Invalid mode. Expected 'edit'"})
if not file.filename or not file.filename.endswith(".zip"):
raise HTTPException(status_code=400, detail="Only .zip files are supported.")
raise HTTPException(status_code=400, detail={"summary": "Only .zip files are supported."})

MAX_FILE_SIZE = 200 * 1024 * 1024

Expand All @@ -278,13 +278,13 @@ async def import_zip(file: UploadFile = File(...)):
if size > MAX_FILE_SIZE:
tmp.close()
os.unlink(tmp.name)
raise HTTPException(status_code=413, detail=f"File too large. Max file size: {MAX_FILE_SIZE}")
raise HTTPException(status_code=413, detail={"summary": f"File too large. Max file size: {MAX_FILE_SIZE}"})
tmp.write(chunk)
tmp_path = tmp.name
await app_runner.import_zip(tmp_path)
os.remove(tmp_path)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid upload.")
except ValueError as e:
raise HTTPException(status_code=400, detail={"summary": "Invalid archive contents", "details": traceback.format_exc()}) from e

@app.post("/api/autogen")
async def autogen(requestBody: AutogenRequestBody, request: Request):
Expand Down