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
225 changes: 147 additions & 78 deletions src/app/(main)/agents/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,23 @@ export default function AgentConfigurationPage({
(agent) => agent.databaseId === id || agent.id === id
);

if (!agent) {
return <div> Agent not found </div>;
}
const [dragActive, setDragActive] = useState(false);
const [embeddingError, setEmbeddingError] = useState<{
show: boolean;
title: string;
message: string;
fileName: string;
}>({
show: false,
title: "",
message: "",
fileName: "",
});
const [activeTab, setActiveTab] = useState("description");

// Load documents when the page opens
useEffect(() => {
if (!agent) return;
// Only fetch if agent has a database ID and documents haven't been loaded yet
if (agent.databaseId && agent.documents === null) {
agentsClient
Expand All @@ -93,9 +104,10 @@ export default function AgentConfigurationPage({
setAgent(agent.id, (prev) => ({ ...prev, documents: [] }));
});
}
}, [agent.databaseId, agent.documents, agent.id, setAgent]);
}, [agent?.databaseId, agent?.documents, agent?.id, setAgent]);

const registerUpdate = () => {
const registerUpdate = useCallback(() => {
if (!agent) return;
const last_updated = new Date().toLocaleString("nb-NO", {
dateStyle: "short",
timeStyle: "short",
Expand All @@ -105,32 +117,11 @@ export default function AgentConfigurationPage({
uploaded: false,
lastUpdated: last_updated,
}));
};

const [dragActive, setDragActive] = useState(false);
const [embeddingError, setEmbeddingError] = useState<{
show: boolean;
title: string;
message: string;
fileName: string;
}>({
show: false,
title: "",
message: "",
fileName: "",
});
const [activeTab, setActiveTab] = useState("description");

const handleInputChange = (
field: keyof AgentUIState,
value: string | number | boolean | null
) => {
registerUpdate();
setAgent(agent.id, (prev) => ({ ...prev, [field]: value }));
};
}, [agent, setAgent]);

const handleFileUpload = useCallback(
async (files: FileList) => {
if (!agent) return;
registerUpdate();

// Create temporary IDs for UI tracking
Expand Down Expand Up @@ -218,14 +209,69 @@ export default function AgentConfigurationPage({
const result = response.data;
console.log(`Successfully uploaded ${file.name}:`, result);

// Update document with actual ID from backend and set status to ready
// Update document with actual ID from backend - keep status as processing
// since the backend now processes asynchronously
setDocuments(agent.id, (prev) =>
prev.map((doc) =>
doc.id === tempIds[index]
? { ...doc, id: result.document_id, status: "ready" as const }
? {
...doc,
id: result.document_id,
// Status stays as "processing" until we poll and confirm it's ready
}
: doc
)
);

// Poll for upload status if task_id is provided
if (result.task_id) {
const pollStatus = async () => {
try {
const statusResponse = await axios.get(
`/api/upload-status?taskId=${result.task_id}`
);
const statusData = statusResponse.data;

if (statusData.status === "complete") {
// Update to ready when complete
setDocuments(agent.id, (prev) =>
prev.map((doc) =>
doc.id === result.document_id
? { ...doc, status: "ready" as const }
: doc
)
);
} else if (statusData.status === "failed") {
// Update to error if failed
setDocuments(agent.id, (prev) =>
prev.map((doc) =>
doc.id === result.document_id
? { ...doc, status: "error" as const }
: doc
)
);
} else {
// Still processing, poll again after 2 seconds
setTimeout(pollStatus, 2000);
}
} catch (error) {
console.error("Failed to check upload status:", error);
// Don't update status on polling error, keep as processing
}
};

// Start polling after a short delay
setTimeout(pollStatus, 2000);
} else {
// Fallback: If no task_id, assume it's ready (for backward compatibility)
setDocuments(agent.id, (prev) =>
prev.map((doc) =>
doc.id === result.document_id
? { ...doc, status: "ready" as const }
: doc
)
);
}
} catch (error) {
console.error(`Failed to upload ${file.name}:`, error);

Expand All @@ -251,9 +297,74 @@ export default function AgentConfigurationPage({
}
}
},
[agent.id, agent.databaseId]
[agent, registerUpdate, setDocuments, setEmbeddingError]
);

const handleDragOver = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setDragActive(true);
},
[setDragActive]
);

const handleDragLeave = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
},
[setDragActive]
);

const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const files = e.dataTransfer.files;
if (files?.length) {
handleFileUpload(files);
}
},
[handleFileUpload, setDragActive]
);

// Check if there are documents not accessed by any role
const unaccessedDocuments = useMemo(() => {
if (!agent?.documents || agent.documents.length === 0) return [];

const accessedDocIds = new Set<string>();
agent.roles.forEach((role) => {
role.documentAccess.forEach((docId) => accessedDocIds.add(docId));
});

return agent.documents.filter(
(doc) => doc.id && !accessedDocIds.has(doc.id)
);
}, [agent?.documents, agent?.roles]);

// Clamp temperature if it exceeds the allowed max for the selected model
useEffect(() => {
if (!agent) return;
const tempMax = agent.model?.provider?.toLowerCase() === "idun" ? 2 : 1;
if (typeof agent.temperature === "number" && agent.temperature > tempMax) {
registerUpdate();
setAgent(agent.id, (prev) => ({ ...prev, temperature: tempMax }));
}
}, [agent, registerUpdate, setAgent]);

// Early return after all hooks
if (!agent) {
return <div>Agent not found</div>;
}

const handleInputChange = (
field: keyof AgentUIState,
value: string | number | boolean | null
) => {
registerUpdate();
setAgent(agent.id, (prev) => ({ ...prev, [field]: value }));
};

const handleDocumentDelete = async (documentId: string) => {
if (!documentId) {
console.error("Cannot delete document: no ID provided");
Expand Down Expand Up @@ -310,32 +421,10 @@ export default function AgentConfigurationPage({
}
};

const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragActive(true);
}, []);

const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
}, []);

const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault();
setDragActive(false);
const files = e.dataTransfer.files;
if (files?.length) {
handleFileUpload(files);
}
},
[handleFileUpload]
);

const handleSave = () => {
// TODO: Implement save functionality
agentsClient.updateAgent(agent).then((newAgent) => {
setAgent(agent.id, (_) => newAgent);
setAgent(agent.id, () => newAgent);
if (newAgent.databaseId !== agent.databaseId) {
router.replace(`/agents/${newAgent.databaseId}`);
}
Expand All @@ -358,31 +447,9 @@ export default function AgentConfigurationPage({
}
};

// Check if there are documents not accessed by any role
const unaccessedDocuments = useMemo(() => {
if (!agent.documents || agent.documents.length === 0) return [];

const accessedDocIds = new Set<string>();
agent.roles.forEach((role) => {
role.documentAccess.forEach((docId) => accessedDocIds.add(docId));
});

return agent.documents.filter(
(doc) => doc.id && !accessedDocIds.has(doc.id)
);
}, [agent.documents, agent.roles]);

// Determine temperature max based on model provider
const tempMax = agent.model?.provider?.toLowerCase() === "idun" ? 2 : 1;

// Clamp temperature if it exceeds the allowed max for the selected model
useEffect(() => {
if (typeof agent.temperature === "number" && agent.temperature > tempMax) {
registerUpdate();
setAgent(agent.id, (prev) => ({ ...prev, temperature: tempMax }));
}
}, [tempMax, agent.temperature, agent.id]);

return (
<div className="space-y-6">
{/* Header */}
Expand Down Expand Up @@ -466,7 +533,8 @@ export default function AgentConfigurationPage({
<div className="grid gap-2">
<Label htmlFor="name">Agent Name</Label>
<p className="text-muted-foreground text-sm">
The agent will not use this, it's just for your reference.
The agent will not use this, it&apos;s just for your
reference.
</p>
<Input
id="name"
Expand All @@ -478,7 +546,8 @@ export default function AgentConfigurationPage({
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<p className="text-muted-foreground text-sm">
The agent will not use this, it's just for your reference.
The agent will not use this, it&apos;s just for your
reference.
</p>
<Textarea
id="description"
Expand Down
2 changes: 1 addition & 1 deletion src/app/(main)/agents/agent_provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export function AgentProvider({ children }: { children: ReactNode }) {
agentsClient
.getAll()
.then((databaseContent) => {
let agents = agentsClient.convertFromDB(databaseContent);
const agents = agentsClient.convertFromDB(databaseContent);
dispatch({ type: "SET_AGENTS", payload: () => agents });
setIsLoading(false);
})
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/delete-agent/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ export async function GET(req: NextRequest) {
const upstream = await axios.get(`${BACKEND_API_URL}/delete-agent`, {
params: { agent_id: agentId },
headers: {
Authorization: `Bearer ${sessionToken}`
Authorization: `Bearer ${sessionToken}`,
},
});

return new NextResponse(null, {
status: upstream.status
status: upstream.status,
});
}
34 changes: 34 additions & 0 deletions src/app/api/upload-status/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from "next/server";
import { getSessionToken } from "@/lib/hooks/token";

const BACKEND_API_URL = process.env.BACKEND_API_URL!;

export async function GET(req: NextRequest) {
const sessionToken = await getSessionToken(req);
if (!sessionToken)
return NextResponse.json({ error: "No access token" }, { status: 401 });

const { searchParams } = new URL(req.url);
const taskId = searchParams.get("taskId");
if (!taskId)
return NextResponse.json({ error: "Missing taskId" }, { status: 400 });

const upstream = await fetch(`${BACKEND_API_URL}/upload/status/${taskId}`, {
method: "GET",
headers: {
Authorization: `Bearer ${sessionToken}`,
Accept: "application/json",
},
});

if (!upstream.ok) {
const error = await upstream.text();
return NextResponse.json(
{ error: error || "Failed to fetch upload status" },
{ status: upstream.status }
);
}

const data = await upstream.json();
return NextResponse.json(data, { status: 200 });
}
2 changes: 1 addition & 1 deletion src/components/agent-configuration/access-key-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function AccessKeyCard({
onRevoke,
agentId,
}: AccessKeyCardProps) {
const [displayAccessKey, setDisplayAccessKey] = useState<Boolean>(false);
const [displayAccessKey, setDisplayAccessKey] = useState<boolean>(false);

const getStatus = () => {
if (!accessKey.expiry_date) return true;
Expand Down
2 changes: 1 addition & 1 deletion src/components/agent-configuration/access-key-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function AccessKeyModal({
name: string,
expiry_date: Date | null
): Promise<AccessKey | null> => {
var params = new URLSearchParams({
const params = new URLSearchParams({
name: name,
agent_id: agentId,
});
Expand Down