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
3 changes: 3 additions & 0 deletions junction-app/app/api/health/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function GET() {
return Response.json({ status: "healthy" });
}
110 changes: 110 additions & 0 deletions junction-app/app/api/research/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// when the user enters something we need to do the following:
// we need to forward the message to an LLM to extract the primary entity
// we then need to similarity match that entity to a list of our firestore entities
// if a firestore entity is found, the client should then redirect to that entity page
// if no firestore entity is found, then we should trigger the deep_security endpoint
// we should then redirect the user to the reports page while we wait the result

import { ai } from "@/lib/gemini";
import { adminDb } from "@/lib/firebase-admin";

export async function POST(request: Request) {
try {
const { message } = await request.json();

if (!message || typeof message !== "string") {
return Response.json(
{ error: "Message is required and must be a string" },
{ status: 400 },
);
}

// Step 1: Extract the primary entity using Gemini
const entityExtractionPrompt = `You are a helpful assistant that extracts the primary entity from a message.
In this case the primary entity is expected to be a software, tool, package, or library that the user wants to know is safe or not.

Message: ${message}

Instructions:
- Extract ONLY the name of the primary software/tool/package
- Return ONLY the entity name, nothing else
- If multiple entities are mentioned, return the primary one
- Do not include version numbers, descriptions, or explanations

Entity name:`;

const result = await ai.models.generateContent({
model: "gemini-2.0-flash-exp",
contents: entityExtractionPrompt,
});

if (!result.text) {
return Response.json(
{ error: "Could not extract entity from message" },
{ status: 400 },
);
}

const entityName = result.text.trim();

if (!entityName) {
return Response.json(
{ error: "Could not extract entity from message" },
{ status: 400 },
);
}

// Step 2: Query Firestore for existing entity (case-insensitive fuzzy match)
const entitiesRef = adminDb.collection("entities");
const snapshot = await entitiesRef.get();

let matchedEntity = null;
const entityNameLower = entityName.toLowerCase();

// Try exact match first, then fuzzy match
for (const doc of snapshot.docs) {
const data = doc.data();
const storedName = data.name?.toLowerCase() || "";

// Exact match
if (storedName === entityNameLower) {
matchedEntity = { id: doc.id, ...data };
break;
}

// Fuzzy match: check if one contains the other
if (
storedName.includes(entityNameLower) ||
entityNameLower.includes(storedName)
) {
matchedEntity = { id: doc.id, ...data };
break;
}
}

// Step 3: If entity exists, return it
if (matchedEntity) {
return Response.json({
found: true,
entity: matchedEntity,
entityName,
});
}

// Step 4: Entity not found, return entity name for client to trigger deep_security
// The client will call the deep_security endpoint
return Response.json({
found: false,
entityName,
});
} catch (error) {
console.error("Research endpoint error:", error);
return Response.json(
{
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
},
{ status: 500 },
);
}
}
53 changes: 49 additions & 4 deletions junction-app/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ import { useEffect, useState } from "react";
import { useAuth } from "@/contexts/AuthContext";
import { useRouter } from "next/navigation";
import { FileUp, Loader, Upload } from "lucide-react";
import ResearchStreamModal from "@/components/ResearchStreamModal";

export default function DashboardPage() {
const { user } = useAuth();
const router = useRouter();
const [query, setQuery] = useState("");
const [fileName, setFileName] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showStreamModal, setShowStreamModal] = useState(false);
const [streamingEntityName, setStreamingEntityName] = useState("");

useEffect(() => {
if (!user) {
Expand All @@ -19,14 +22,48 @@ export default function DashboardPage() {
}
}, [user, router]);

const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!query.trim() && !fileName) return;
setIsSubmitting(true);
// Placeholder for API request
setTimeout(() => {

try {
// Call the research API
const response = await fetch("/api/research", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ message: query }),
});

const data = await response.json();

if (!response.ok) {
throw new Error(data.error || "Failed to process request");
}

// If entity was found in Firestore
if (data.found) {
router.push(`/entity/${data.entity.id}`);
} else {
// Entity not found, open streaming modal
setStreamingEntityName(data.entityName);
setShowStreamModal(true);
}
} catch (error) {
console.error("Error submitting query:", error);
alert(
error instanceof Error ? error.message : "Failed to process request",
);
} finally {
setIsSubmitting(false);
}, 1500);
}
};

const handleStreamComplete = () => {
// Redirect to reports page after streaming completes
router.push("/reports");
};

const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -135,6 +172,14 @@ export default function DashboardPage() {
</form>
</section>
</div>

{/* Research Stream Modal */}
<ResearchStreamModal
isOpen={showStreamModal}
onClose={() => setShowStreamModal(false)}
entityName={streamingEntityName}
onComplete={handleStreamComplete}
/>
</div>
);
}
Loading