diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 11594161..221cb64e 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -42,6 +42,7 @@ from application.utils import ( generate_image_url, safe_filename, validate_function_name, + validate_required_fields, ) from application.vectorstore.vector_creator import VectorCreator @@ -1234,7 +1235,6 @@ class CreateAgent(Resource): if request.content_type == "application/json": data = request.get_json() else: - print(request.form) data = request.form.to_dict() if "tools" in data: try: @@ -1242,11 +1242,18 @@ class CreateAgent(Resource): except json.JSONDecodeError: data["tools"] = [] print(f"Received data: {data}") + if data.get("status") not in ["draft", "published"]: return make_response( - jsonify({"success": False, "message": "Invalid status"}), 400 + jsonify( + { + "success": False, + "message": "Status must be either 'draft' or 'published'", + } + ), + 400, ) - required_fields = [] + if data.get("status") == "published": required_fields = [ "name", @@ -1257,11 +1264,16 @@ class CreateAgent(Resource): "prompt_id", "agent_type", ] + validate_fields = ["name", "description", "prompt_id", "agent_type"] else: required_fields = ["name"] + validate_fields = [] missing_fields = check_required_fields(data, required_fields) + invalid_fields = validate_required_fields(data, validate_fields) if missing_fields: return missing_fields + if invalid_fields: + return invalid_fields image_url, error = handle_image_upload(request, "", user, storage) if error: @@ -1270,7 +1282,7 @@ class CreateAgent(Resource): ) try: - key = str(uuid.uuid4()) + key = str(uuid.uuid4()) if data.get("status") == "published" else "" new_agent = { "user": user, "name": data.get("name"), @@ -1455,6 +1467,7 @@ class UpdateAgent(Resource): return make_response( jsonify({"success": False, "message": "No update data provided"}), 400 ) + newly_generated_key = None final_status = update_fields.get("status", existing_agent.get("status")) if final_status == "published": required_published_fields = [ @@ -1484,6 +1497,10 @@ class UpdateAgent(Resource): ), 400, ) + + if not existing_agent.get("key"): + newly_generated_key = str(uuid.uuid4()) + update_fields["key"] = newly_generated_key update_fields["updatedAt"] = datetime.datetime.now(datetime.timezone.utc) try: @@ -1506,7 +1523,7 @@ class UpdateAgent(Resource): jsonify( { "success": True, - "message": "Agent found, but no changes were applied.", + "message": "Agent found, but no changes were applied", } ), 304, @@ -1519,14 +1536,15 @@ class UpdateAgent(Resource): jsonify({"success": False, "message": "Database error during update"}), 500, ) + response_data = { + "success": True, + "id": agent_id, + "message": "Agent updated successfully", + } + if newly_generated_key: + response_data["key"] = newly_generated_key return make_response( - jsonify( - { - "success": True, - "id": agent_id, - "message": "Agent updated successfully", - } - ), + jsonify(response_data), 200, ) diff --git a/application/utils.py b/application/utils.py index 5937179d..883eb926 100644 --- a/application/utils.py +++ b/application/utils.py @@ -80,7 +80,7 @@ def check_required_fields(data, required_fields): jsonify( { "success": False, - "message": f"Missing fields: {', '.join(missing_fields)}", + "message": f"Missing required fields: {', '.join(missing_fields)}", } ), 400, @@ -88,6 +88,29 @@ def check_required_fields(data, required_fields): return None +def validate_required_fields(data, required_fields): + missing_fields = [] + empty_fields = [] + + for field in required_fields: + if field not in data: + missing_fields.append(field) + elif not data[field]: + empty_fields.append(field) + + errors = [] + if missing_fields: + errors.append(f"Missing required fields: {', '.join(missing_fields)}") + if empty_fields: + errors.append(f"Empty values in required fields: {', '.join(empty_fields)}") + + if errors: + return make_response( + jsonify({"success": False, "message": " | ".join(errors)}), 400 + ) + return None + + def get_hash(data): return hashlib.md5(data.encode(), usedforsecurity=False).hexdigest() diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e7c7165d..56a757b4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "copy-to-clipboard": "^3.3.3", "i18next": "^24.2.0", "i18next-browser-languagedetector": "^8.0.2", + "lodash": "^4.17.21", "mermaid": "^11.6.0", "prop-types": "^15.8.1", "react": "^19.1.0", @@ -32,6 +33,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.10", + "@types/lodash": "^4.17.20", "@types/mermaid": "^9.1.0", "@types/react": "^19.1.8", "@types/react-dom": "^19.0.0", @@ -2302,6 +2304,13 @@ "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -7226,6 +7235,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", diff --git a/frontend/package.json b/frontend/package.json index dc434230..cb334bed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "copy-to-clipboard": "^3.3.3", "i18next": "^24.2.0", "i18next-browser-languagedetector": "^8.0.2", + "lodash": "^4.17.21", "mermaid": "^11.6.0", "prop-types": "^15.8.1", "react": "^19.1.0", @@ -43,6 +44,7 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4.1.10", + "@types/lodash": "^4.17.20", "@types/mermaid": "^9.1.0", "@types/react": "^19.1.8", "@types/react-dom": "^19.0.0", diff --git a/frontend/src/agents/AgentPreview.tsx b/frontend/src/agents/AgentPreview.tsx index 8798a155..ab63c06b 100644 --- a/frontend/src/agents/AgentPreview.tsx +++ b/frontend/src/agents/AgentPreview.tsx @@ -110,7 +110,7 @@ export default function AgentPreview() { }, [queries]); return (
-
+
-

+

This is a preview of the agent. You can publish it to start using it in conversations.

diff --git a/frontend/src/agents/NewAgent.tsx b/frontend/src/agents/NewAgent.tsx index 2162191a..b3047fa9 100644 --- a/frontend/src/agents/NewAgent.tsx +++ b/frontend/src/agents/NewAgent.tsx @@ -1,3 +1,4 @@ +import isEqual from 'lodash/isEqual'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigate, useParams } from 'react-router-dom'; @@ -8,6 +9,7 @@ import SourceIcon from '../assets/source.svg'; import Dropdown from '../components/Dropdown'; import { FileUpload } from '../components/FileUpload'; import MultiSelectPopup, { OptionType } from '../components/MultiSelectPopup'; +import Spinner from '../components/Spinner'; import AgentDetailsModal from '../modals/AgentDetailsModal'; import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState, Doc, Prompt } from '../models/misc'; @@ -66,34 +68,41 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { useState('INACTIVE'); const [agentDetails, setAgentDetails] = useState('INACTIVE'); const [addPromptModal, setAddPromptModal] = useState('INACTIVE'); + const [hasChanges, setHasChanges] = useState(false); + const [draftLoading, setDraftLoading] = useState(false); + const [publishLoading, setPublishLoading] = useState(false); + const initialAgentRef = useRef(null); const sourceAnchorButtonRef = useRef(null); const toolAnchorButtonRef = useRef(null); const modeConfig = { new: { heading: 'New Agent', - buttonText: 'Create Agent', + buttonText: 'Publish', showDelete: false, showSaveDraft: true, showLogs: false, showAccessDetails: false, + trackChanges: false, }, edit: { heading: 'Edit Agent', - buttonText: 'Save Changes', + buttonText: 'Save', showDelete: true, showSaveDraft: false, showLogs: true, showAccessDetails: true, + trackChanges: true, }, draft: { heading: 'New Agent (Draft)', - buttonText: 'Publish Draft', + buttonText: 'Publish', showDelete: true, showSaveDraft: true, showLogs: false, showAccessDetails: false, + trackChanges: false, }, }; const chunks = ['0', '2', '4', '6', '8', '10']; @@ -144,23 +153,27 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { else formData.append('tools', '[]'); try { + setDraftLoading(true); const response = effectiveMode === 'new' ? await userService.createAgent(formData, token) : await userService.updateAgent(agent.id || '', formData, token); if (!response.ok) throw new Error('Failed to create agent draft'); const data = await response.json(); - if (effectiveMode === 'new') { - setEffectiveMode('draft'); - setAgent((prev) => ({ - ...prev, - id: data.id, - image: data.image || prev.image, - })); - } + + const updatedAgent = { + ...agent, + id: data.id || agent.id, + image: data.image || agent.image, + }; + setAgent(updatedAgent); + + if (effectiveMode === 'new') setEffectiveMode('draft'); } catch (error) { console.error('Error saving draft:', error); throw new Error('Failed to save draft'); + } finally { + setDraftLoading(false); } }; @@ -181,26 +194,34 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { else formData.append('tools', '[]'); try { + setPublishLoading(true); const response = effectiveMode === 'new' ? await userService.createAgent(formData, token) : await userService.updateAgent(agent.id || '', formData, token); if (!response.ok) throw new Error('Failed to publish agent'); const data = await response.json(); - if (data.id) setAgent((prev) => ({ ...prev, id: data.id })); - if (data.key) setAgent((prev) => ({ ...prev, key: data.key })); + + const updatedAgent = { + ...agent, + id: data.id || agent.id, + key: data.key || agent.key, + status: 'published', + image: data.image || agent.image, + }; + setAgent(updatedAgent); + initialAgentRef.current = updatedAgent; + if (effectiveMode === 'new' || effectiveMode === 'draft') { setEffectiveMode('edit'); - setAgent((prev) => ({ - ...prev, - status: 'published', - image: data.image || prev.image, - })); setAgentDetails('ACTIVE'); } + setImageFile(null); } catch (error) { console.error('Error publishing agent:', error); throw new Error('Failed to publish agent'); + } finally { + setPublishLoading(false); } }; @@ -243,6 +264,7 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { if (data.tools) setSelectedToolIds(new Set(data.tools)); if (data.status === 'draft') setEffectiveMode('draft'); setAgent(data); + initialAgentRef.current = data; }; getAgent(); } @@ -285,7 +307,19 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { useEffect(() => { if (isPublishable()) dispatch(setSelectedAgent(agent)); - }, [agent, dispatch]); + + if (!modeConfig[effectiveMode].trackChanges) { + setHasChanges(true); + return; + } + if (!initialAgentRef.current) { + setHasChanges(false); + return; + } + const isChanged = + !isEqual(agent, initialAgentRef.current) || imageFile !== null; + setHasChanges(isChanged); + }, [agent, dispatch, effectiveMode, imageFile]); return (
@@ -321,10 +355,16 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { )} {modeConfig[effectiveMode].showSaveDraft && ( )} {modeConfig[effectiveMode].showAccessDetails && ( @@ -345,11 +385,17 @@ export default function NewAgent({ mode }: { mode: 'new' | 'edit' | 'draft' }) { )}
@@ -654,7 +700,7 @@ function AddPromptModal({ setNewPromptContent(''); onSelect?.(newPromptName, newPrompt.id, newPromptContent); } catch (error) { - console.error(error); + console.error('Error adding prompt:', error); } }; return (