diff --git a/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/CodebaseVisualizer.tsx b/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/CodebaseVisualizer.tsx index 0220c20..e4885f4 100644 --- a/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/CodebaseVisualizer.tsx +++ b/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/CodebaseVisualizer.tsx @@ -1,11 +1,13 @@ "use client"; -import React, { useState, useMemo, useEffect } from "react"; +import React, { useState, useMemo, useEffect, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Tree } from "./Tree"; import CodebaseDetails from "./CodebaseDetails"; import { type ContextItem } from "~/server/utils/codebaseContext"; import { type FileType } from "./types"; +import SearchBar from "./SearchBar"; +import SearchResults from "./SearchResults"; interface CodebaseVisualizerProps { contextItems: ContextItem[]; @@ -26,6 +28,8 @@ export const CodebaseVisualizer: React.FC = ({ const [detailsWidth, setDetailsWidth] = useState(30); const [allFiles, setAllFiles] = useState([]); const [viewMode, setViewMode] = useState<"folder" | "taxonomy">("folder"); + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); useEffect(() => { setIsMounted(true); @@ -129,6 +133,21 @@ export const CodebaseVisualizer: React.FC = ({ setDetailsWidth(detailsWidth === 30 ? 50 : 30); }; + const handleSearch = useCallback((results: ContextItem[]) => { + setSearchResults(results); + setIsSearching(true); + }, []); + + const handleSearchResultSelect = useCallback((filePath: string) => { + handleNodeClick(filePath); + setIsSearching(false); + }, [handleNodeClick]); + + const handleCloseSearch = useCallback(() => { + setSearchResults([]); + setIsSearching(false); + }, []); + if (!isMounted) { return null; } @@ -139,6 +158,7 @@ export const CodebaseVisualizer: React.FC = ({
+ {currentPath.map((part, index) => ( @@ -187,24 +207,32 @@ export const CodebaseVisualizer: React.FC = ({ transition={{ duration: 0.3 }} > - + {isSearching ? ( + + ) : ( + + )}
{selectedItem && ( diff --git a/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/SearchBar.tsx b/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/SearchBar.tsx index 9e59922..82e5647 100644 --- a/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/SearchBar.tsx +++ b/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/SearchBar.tsx @@ -1,34 +1,48 @@ // components/SearchBar.tsx -import React, { useState } from "react"; +import React, { useState, useCallback } from "react"; import { motion } from "framer-motion"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import { api } from "~/trpc/react"; +import { type ContextItem } from "~/server/utils/codebaseContext"; interface SearchBarProps { - onSearch: (term: string) => void; + org: string; + repo: string; + onSearch: (results: ContextItem[]) => void; } -const SearchBar: React.FC = ({ onSearch }) => { +const SearchBar: React.FC = ({ org, repo, onSearch }) => { const [searchTerm, setSearchTerm] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + const searchCodebase = api.codebaseContext.searchCodebase.useQuery(); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault(); - onSearch(searchTerm); + setIsLoading(true); + const results = await searchCodebase.refetch({ org, repo, query: searchTerm }); + setIsLoading(false); + if (results.data) { + onSearch(results.data); + } }; return (
setSearchTerm(e.target.value)} placeholder="Search files..." className="rounded-l-lg bg-blueGray-700 px-4 py-2 text-gray-300 focus:outline-none focus:ring-2 focus:ring-light-blue" - whileFocus={{ scale: 1.05 }} + className={`rounded-r-lg px-4 py-2 text-white ${isLoading ? 'bg-gray-500' : 'bg-light-blue'}`} /> } whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > diff --git a/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/SearchResults.tsx b/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/SearchResults.tsx new file mode 100644 index 0000000..6f1b631 --- /dev/null +++ b/src/app/dashboard/[org]/[repo]/code-visualizer/codebase/SearchResults.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { motion } from "framer-motion"; +import { type ContextItem } from "~/server/utils/codebaseContext"; + +interface SearchResultsProps { + results: ContextItem[]; + onSelect: (filePath: string) => void; +} + +const SearchResults: React.FC = ({ results, onSelect }) => { + return ( +
+ {results.length > 0 ? ( + + {results.slice(0, 10).map((result, index) => ( + onSelect(result.filePath)} + > +
+ {result.filePath} +
+
+ {result.overview} +
+
+ ))} +
+ ) : ( +
+ No matching files found. +
+ )} +
+ ); +}; + +export default SearchResults; diff --git a/src/server/api/routers/codebaseContext.ts b/src/server/api/routers/codebaseContext.ts index d5d2617..5a7113f 100644 --- a/src/server/api/routers/codebaseContext.ts +++ b/src/server/api/routers/codebaseContext.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { db } from "~/server/db/db"; import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; +import { searchCodebase } from "~/server/ai/search"; import { type ContextItem } from "~/server/utils/codebaseContext"; import { Octokit } from "@octokit/rest"; import { TRPCError } from "@trpc/server"; @@ -99,6 +100,30 @@ export const codebaseContextRouter = createTRPCRouter({ await generateCodebaseContext(org, repoName, accessToken); }, ), + searchCodebase: protectedProcedure + .input( + z.object({ + org: z.string(), + repo: z.string(), + query: z.string(), + }), + ) + .query( + async ({ + input: { org, repo, query }, + ctx: { + session: { accessToken }, + }, + }): Promise => { + if (!accessToken) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Not authenticated", + }); + } + return await searchCodebase(org, repo, query, accessToken); + }, + ), }); const generateCodebaseContext = async (