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
20 changes: 18 additions & 2 deletions app/(main)/keystore/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import AddKeyModal from "@/app/components/keystore/AddKeyModal";
import { useAuth } from "@/app/lib/context/AuthContext";
import { useApp } from "@/app/lib/context/AppContext";
import { useToast } from "@/app/components/Toast";
import { apiFetch } from "@/app/lib/apiClient";
import { APIKey } from "@/app/lib/types/credentials";

export const STORAGE_KEY = "kaapi_api_keys";
Expand All @@ -28,30 +29,44 @@ export default function KaapiKeystore() {
const [newKeyProvider, setNewKeyProvider] = useState("Kaapi");
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set());
const [copiedKeyId, setCopiedKeyId] = useState<string | null>(null);
const [isValidating, setIsValidating] = useState(false);

const resetForm = () => {
setNewKeyLabel("");
setNewKeyValue("");
setNewKeyProvider("Kaapi");
};

const handleAddKey = () => {
const handleAddKey = async () => {
if (!newKeyLabel.trim() || !newKeyValue.trim()) {
toast.error("Please provide both a label and an API key");
return;
}

const trimmedKey = newKeyValue.trim();
setIsValidating(true);

try {
await apiFetch("/api/apikeys/verify", trimmedKey);
} catch (err) {
console.error("API key validation failed:", err);
toast.error("Invalid API key. Please check the key and try again.");
setIsValidating(false);
return;
}

const newKey: APIKey = {
id: Date.now().toString(),
label: newKeyLabel.trim(),
key: newKeyValue.trim(),
key: trimmedKey,
provider: newKeyProvider,
createdAt: new Date().toISOString(),
};

addKey(newKey);
resetForm();
setIsModalOpen(false);
setIsValidating(false);
toast.success("API key added successfully");
};
Comment on lines +49 to 71
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add a finally block to guarantee isValidating state cleanup.

If an unexpected error occurs after the validation succeeds (lines 58–70)—for example, if addKey throws or an unforeseen exception arises—isValidating will remain true, permanently disabling the modal buttons.

Wrap the entire operation in a try-finally or extend the existing try-catch with a finally block to reset the state reliably.

🛡️ Proposed fix to add finally block
-    const trimmedKey = newKeyValue.trim();
-    setIsValidating(true);
-
-    try {
-      await apiFetch("/api/apikeys/verify", trimmedKey);
-    } catch (err) {
-      console.error("API key validation failed:", err);
-      toast.error("Invalid API key. Please check the key and try again.");
-      setIsValidating(false);
-      return;
-    }
-
-    const newKey: APIKey = {
-      id: Date.now().toString(),
-      label: newKeyLabel.trim(),
-      key: trimmedKey,
-      provider: newKeyProvider,
-      createdAt: new Date().toISOString(),
-    };
-
-    addKey(newKey);
-    resetForm();
-    setIsModalOpen(false);
-    setIsValidating(false);
-    toast.success("API key added successfully");
+    const trimmedKey = newKeyValue.trim();
+    setIsValidating(true);
+
+    try {
+      await apiFetch("/api/apikeys/verify", trimmedKey);
+
+      const newKey: APIKey = {
+        id: Date.now().toString(),
+        label: newKeyLabel.trim(),
+        key: trimmedKey,
+        provider: newKeyProvider,
+        createdAt: new Date().toISOString(),
+      };
+
+      addKey(newKey);
+      resetForm();
+      setIsModalOpen(false);
+      toast.success("API key added successfully");
+    } catch (err) {
+      console.error("API key validation failed:", err);
+      toast.error("Invalid API key. Please check the key and try again.");
+    } finally {
+      setIsValidating(false);
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(main)/keystore/page.tsx around lines 49 - 71, The isValidating flag may
remain true if an exception occurs after API validation; wrap the whole add-key
flow in a try { ... } finally { setIsValidating(false); } (or add a finally to
the existing try-catch) so setIsValidating(false) always runs; ensure you
include the block that calls apiFetch("/api/apikeys/verify", ...), constructs
newKey, and calls addKey, resetForm, setIsModalOpen and toast.success inside the
try so cleanup always happens in the finally.


Expand Down Expand Up @@ -122,6 +137,7 @@ export default function KaapiKeystore() {
newKeyLabel={newKeyLabel}
newKeyValue={newKeyValue}
newKeyProvider={newKeyProvider}
isValidating={isValidating}
onLabelChange={setNewKeyLabel}
onValueChange={setNewKeyValue}
onProviderChange={setNewKeyProvider}
Expand Down
22 changes: 17 additions & 5 deletions app/(main)/knowledge-base/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default function KnowledgeBasePage() {
);
const [showDocPreviewModal, setShowDocPreviewModal] = useState(false);
const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
const [isPreviewLoading, setIsPreviewLoading] = useState(false);

const [collectionName, setCollectionName] = useState("");
const [collectionDescription, setCollectionDescription] = useState("");
Expand Down Expand Up @@ -103,14 +104,24 @@ export default function KnowledgeBasePage() {
const handlePreviewDocument = async (firstDocument: Document) => {
setShowDocPreviewModal(true);
setPreviewDoc(firstDocument);
const enriched = await fetchAndPreviewDoc(firstDocument);
setPreviewDoc(enriched);
setIsPreviewLoading(true);
try {
const enriched = await fetchAndPreviewDoc(firstDocument);
setPreviewDoc(enriched);
} finally {
setIsPreviewLoading(false);
}
};

const handleSelectPreviewDoc = async (doc: Document) => {
setPreviewDoc(doc);
const enriched = await fetchAndPreviewDoc(doc);
setPreviewDoc(enriched);
setIsPreviewLoading(true);
try {
const enriched = await fetchAndPreviewDoc(doc);
setPreviewDoc(enriched);
} finally {
setIsPreviewLoading(false);
}
};
Comment on lines 104 to 125
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Race condition: Rapid document selection can display stale previews.

If a user clicks document A, then quickly clicks document B before A's preview loads, the requests can complete out of order. When A finishes after B, setPreviewDoc(enrichedA) will overwrite B, showing the wrong document to the user.

🔒 Proposed fix using request ID tracking
 export default function KnowledgeBasePage() {
   ...
   const [previewDoc, setPreviewDoc] = useState<Document | null>(null);
   const [isPreviewLoading, setIsPreviewLoading] = useState(false);
+  const previewRequestIdRef = useRef(0);

   const handlePreviewDocument = async (firstDocument: Document) => {
     setShowDocPreviewModal(true);
     setPreviewDoc(firstDocument);
+    const requestId = ++previewRequestIdRef.current;
     setIsPreviewLoading(true);
     try {
       const enriched = await fetchAndPreviewDoc(firstDocument);
-      setPreviewDoc(enriched);
+      if (requestId === previewRequestIdRef.current) {
+        setPreviewDoc(enriched);
+      }
     } finally {
-      setIsPreviewLoading(false);
+      if (requestId === previewRequestIdRef.current) {
+        setIsPreviewLoading(false);
+      }
     }
   };

   const handleSelectPreviewDoc = async (doc: Document) => {
     setPreviewDoc(doc);
+    const requestId = ++previewRequestIdRef.current;
     setIsPreviewLoading(true);
     try {
       const enriched = await fetchAndPreviewDoc(doc);
-      setPreviewDoc(enriched);
+      if (requestId === previewRequestIdRef.current) {
+        setPreviewDoc(enriched);
+      }
     } finally {
-      setIsPreviewLoading(false);
+      if (requestId === previewRequestIdRef.current) {
+        setIsPreviewLoading(false);
+      }
     }
   };

Don't forget to add the import:

-import { useState } from "react";
+import { useState, useRef } from "react";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handlePreviewDocument = async (firstDocument: Document) => {
setShowDocPreviewModal(true);
setPreviewDoc(firstDocument);
const enriched = await fetchAndPreviewDoc(firstDocument);
setPreviewDoc(enriched);
setIsPreviewLoading(true);
try {
const enriched = await fetchAndPreviewDoc(firstDocument);
setPreviewDoc(enriched);
} finally {
setIsPreviewLoading(false);
}
};
const handleSelectPreviewDoc = async (doc: Document) => {
setPreviewDoc(doc);
const enriched = await fetchAndPreviewDoc(doc);
setPreviewDoc(enriched);
setIsPreviewLoading(true);
try {
const enriched = await fetchAndPreviewDoc(doc);
setPreviewDoc(enriched);
} finally {
setIsPreviewLoading(false);
}
};
const handlePreviewDocument = async (firstDocument: Document) => {
setShowDocPreviewModal(true);
setPreviewDoc(firstDocument);
const requestId = ++previewRequestIdRef.current;
setIsPreviewLoading(true);
try {
const enriched = await fetchAndPreviewDoc(firstDocument);
if (requestId === previewRequestIdRef.current) {
setPreviewDoc(enriched);
}
} finally {
if (requestId === previewRequestIdRef.current) {
setIsPreviewLoading(false);
}
}
};
const handleSelectPreviewDoc = async (doc: Document) => {
setPreviewDoc(doc);
const requestId = ++previewRequestIdRef.current;
setIsPreviewLoading(true);
try {
const enriched = await fetchAndPreviewDoc(doc);
if (requestId === previewRequestIdRef.current) {
setPreviewDoc(enriched);
}
} finally {
if (requestId === previewRequestIdRef.current) {
setIsPreviewLoading(false);
}
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(main)/knowledge-base/page.tsx around lines 104 - 125, Both handlers
(handlePreviewDocument and handleSelectPreviewDoc) suffer from a race where
out-of-order fetches overwrite the preview; fix by adding a per-component
request tracker (e.g., a useRef<number> latestPreviewRequestId) that you
increment before calling fetchAndPreviewDoc, capture the current id in the async
closure, and only call setPreviewDoc(enriched) (and setIsPreviewLoading(false)
behaviorally) if the captured id matches latestPreviewRequestId.current; update
setShowDocPreviewModal as before and keep using fetchAndPreviewDoc (or switch to
AbortController if supported) so only the most recent selection updates the UI.


return (
Expand Down Expand Up @@ -153,7 +164,7 @@ export default function KnowledgeBasePage() {
<Loader size="md" message="Loading knowledge base…" />
</div>
) : selectedCollection ? (
<div className="flex-1 flex flex-col relative">
<div className="flex-1 min-h-0 flex flex-col relative">
<CollectionDetail
collection={selectedCollection}
onRequestDelete={handleRequestDelete}
Expand Down Expand Up @@ -257,6 +268,7 @@ export default function KnowledgeBasePage() {
}}
documents={selectedCollection?.documents ?? []}
previewDoc={previewDoc}
isLoading={isPreviewLoading}
onSelectDocument={handleSelectPreviewDoc}
/>
</div>
Expand Down
4 changes: 2 additions & 2 deletions app/api/collections/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { apiClient } from "@/app/lib/apiClient";

export async function GET(request: Request) {
try {
const { status, data } = await apiClient(request, "/api/v1/collections/");
const { status, data } = await apiClient(request, "/api/v1/collections");
return NextResponse.json(data, { status });
} catch (error: unknown) {
return NextResponse.json(
Expand All @@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
// Get the JSON body from the request
const body = await request.json();

const { status, data } = await apiClient(request, "/api/v1/collections/", {
const { status, data } = await apiClient(request, "/api/v1/collections", {
method: "POST",
body: JSON.stringify(body),
});
Expand Down
4 changes: 2 additions & 2 deletions app/api/configs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const queryString = searchParams.toString();
const endpoint = `/api/v1/configs/${queryString ? `?${queryString}` : ""}`;
const endpoint = `/api/v1/configs${queryString ? `?${queryString}` : ""}`;
const { status, data } = await apiClient(request, endpoint);
return NextResponse.json(data, { status });
} catch (error) {
Expand All @@ -24,7 +24,7 @@ export async function POST(request: Request) {
try {
const body = await request.json();

const { status, data } = await apiClient(request, "/api/v1/configs/", {
const { status, data } = await apiClient(request, "/api/v1/configs", {
method: "POST",
body: JSON.stringify(body),
});
Expand Down
6 changes: 3 additions & 3 deletions app/api/credentials/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { NextResponse, NextRequest } from "next/server";

export async function GET(request: NextRequest) {
try {
const { status, data } = await apiClient(request, "/api/v1/credentials/");
const { status, data } = await apiClient(request, "/api/v1/credentials");
return NextResponse.json(data, { status });
} catch (e: unknown) {
return NextResponse.json(
Expand All @@ -16,7 +16,7 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { status, data } = await apiClient(request, "/api/v1/credentials/", {
const { status, data } = await apiClient(request, "/api/v1/credentials", {
method: "POST",
body: JSON.stringify(body),
});
Expand All @@ -32,7 +32,7 @@ export async function POST(request: NextRequest) {
export async function PATCH(request: NextRequest) {
try {
const body = await request.json();
const { status, data } = await apiClient(request, "/api/v1/credentials/", {
const { status, data } = await apiClient(request, "/api/v1/credentials", {
method: "PATCH",
body: JSON.stringify(body),
});
Expand Down
4 changes: 2 additions & 2 deletions app/api/document/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const queryString = searchParams.toString();
const endpoint = `/api/v1/documents/${queryString ? `?${queryString}` : ""}`;
const endpoint = `/api/v1/documents${queryString ? `?${queryString}` : ""}`;
const { status, data } = await apiClient(request, endpoint);
return NextResponse.json(data, { status });
} catch (error: unknown) {
Expand Down Expand Up @@ -78,7 +78,7 @@ export async function POST(request: NextRequest) {
},
});

const { status, data } = await apiClient(request, "/api/v1/documents/", {
const { status, data } = await apiClient(request, "/api/v1/documents", {
method: "POST",
body: uploadBody,
headers: { "Content-Type": contentType },
Expand Down
2 changes: 1 addition & 1 deletion app/api/organization/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function GET(request: NextRequest) {

const { status, data } = await apiClient(
request,
`/api/v1/organizations/${queryString ? `?${queryString}` : ""}`,
`/api/v1/organizations${queryString ? `?${queryString}` : ""}`,
);
return NextResponse.json(data, { status });
} catch {
Expand Down
2 changes: 1 addition & 1 deletion app/api/projects/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { apiClient } from "@/app/lib/apiClient";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { status, data } = await apiClient(request, "/api/v1/projects/", {
const { status, data } = await apiClient(request, "/api/v1/projects", {
method: "POST",
body: JSON.stringify(body),
});
Expand Down
14 changes: 5 additions & 9 deletions app/api/user-projects/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function GET(request: NextRequest) {

const { status, data } = await apiClient(
request,
`/api/v1/user-projects/${queryString ? `?${queryString}` : ""}`,
`/api/v1/user-projects${queryString ? `?${queryString}` : ""}`,
);
return NextResponse.json(data, { status });
} catch {
Expand All @@ -22,14 +22,10 @@ export async function GET(request: NextRequest) {
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { status, data } = await apiClient(
request,
"/api/v1/user-projects/",
{
method: "POST",
body: JSON.stringify(body),
},
);
const { status, data } = await apiClient(request, "/api/v1/user-projects", {
method: "POST",
body: JSON.stringify(body),
});
return NextResponse.json(data, { status });
} catch {
return NextResponse.json(
Expand Down
2 changes: 1 addition & 1 deletion app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ export default function Sidebar({
</button>
</div>
) : !isAuthenticated ? (
<div className="px-4 py-4 w-60 bg-bg-secondary">
<div className="px-4 py-4 w-60 bg-bg-primary border-t border-r border-border">
<div className="p-2">
<p className="text-sm font-bold text-text-primary">
Get full access
Expand Down
9 changes: 6 additions & 3 deletions app/components/keystore/AddKeyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface AddKeyModalProps {
newKeyLabel: string;
newKeyValue: string;
newKeyProvider: string;
isValidating?: boolean;
onLabelChange: (value: string) => void;
onValueChange: (value: string) => void;
onProviderChange: (value: string) => void;
Expand All @@ -23,13 +24,15 @@ export default function AddKeyModal({
newKeyLabel,
newKeyValue,
newKeyProvider,
isValidating,
onLabelChange,
onValueChange,
onProviderChange,
onAddKey,
onClose,
}: AddKeyModalProps) {
const isDisabled = !newKeyLabel.trim() || !newKeyValue.trim();
const isDisabled =
!newKeyLabel.trim() || !newKeyValue.trim() || !!isValidating;

return (
<Modal
Expand Down Expand Up @@ -83,11 +86,11 @@ export default function AddKeyModal({
</div>

<div className="border-t border-border px-6 py-4 flex items-center justify-end gap-3 shrink-0">
<Button variant="outline" onClick={onClose}>
<Button variant="outline" onClick={onClose} disabled={isValidating}>
Cancel
</Button>
<Button variant="primary" onClick={onAddKey} disabled={isDisabled}>
Add API Key
{isValidating ? "Validating…" : "Add API Key"}
</Button>
</div>
</Modal>
Expand Down
2 changes: 1 addition & 1 deletion app/components/keystore/KeysCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export default function KeysCard({
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2 flex-wrap">
<span className="text-xs font-medium px-2 py-0.5 rounded-full bg-text-primary text-bg-primary">
<span className="text-xs font-medium px-2.5 py-0.5 rounded-full bg-accent-primary/10 text-accent-primary border border-accent-primary/20">
{apiKey.provider}
</span>
<h3 className="text-sm font-semibold text-text-primary truncate">
Expand Down
16 changes: 10 additions & 6 deletions app/components/knowledge-base/CollectionDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ export default function CollectionDetail({
</div>
</div>

<div className="flex-1 overflow-y-auto px-6 pt-6 pb-4">
<div className="flex items-center justify-between mb-4">
<div className="flex-1 min-h-0 px-6 pt-6 pb-4 flex flex-col">
<div className="flex items-center justify-between mb-4 shrink-0">
<h3 className="text-sm font-semibold text-text-primary">
Documents Present ({documents.length})
</h3>
Expand All @@ -126,8 +126,8 @@ export default function CollectionDetail({
</div>

{documents.length > 0 ? (
<div>
<div className="flex items-center justify-between pb-2 mb-2 border-b border-border">
<div className="flex-1 min-h-0 flex flex-col">
<div className="flex items-center justify-between pb-2 mb-2 border-b border-border shrink-0">
<div className="flex-1">
<p className="text-[10px] font-semibold uppercase text-text-secondary">
Name
Expand All @@ -140,7 +140,11 @@ export default function CollectionDetail({
</div>
</div>

<div className="space-y-2">
<div
className={`space-y-2 ${
showAllDocs ? "flex-1 min-h-0 overflow-y-auto" : ""
}`}
>
{visibleDocs.map((doc) => (
<div
key={doc.id}
Expand All @@ -161,7 +165,7 @@ export default function CollectionDetail({
</div>

{documents.length > 3 && (
<div className="mt-2 flex justify-center">
<div className="mt-2 flex justify-center shrink-0">
<button
type="button"
onClick={() => setShowAllDocs(!showAllDocs)}
Expand Down
Loading
Loading