From 9781ec023b421754254dddcbdd1f30f6fe8e6bff Mon Sep 17 00:00:00 2001 From: Kevin Lu Date: Wed, 5 Jun 2024 11:07:29 -0700 Subject: [PATCH 1/7] E2E PR Test --- sweep_chat/components/App.tsx | 67 +++++++++++++++++-------------- sweep_chat/cypress/e2e/spec.cy.ts | 10 ++++- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/sweep_chat/components/App.tsx b/sweep_chat/components/App.tsx index 47cf850409..91c4df76f0 100644 --- a/sweep_chat/components/App.tsx +++ b/sweep_chat/components/App.tsx @@ -522,37 +522,44 @@ const parsePullRequests = async (repoName: string, message: string, octokit: Oct const [orgName, repo] = repoName.split("/") const pulls = [] - const prURLs = message.match(new RegExp(`https?:\/\/github.com\/${repoName}\/pull\/(?[0-9]+)`, 'gm')); - for (const prURL of prURLs || []) { - const prNumber = prURL.split("/").pop() - console.log(prNumber) - const pr = await octokit!.rest.pulls.get({ - owner: orgName, - repo: repo, - pull_number: parseInt(prNumber!) - }) - const title = pr.data.title - const body = pr.data.body - const labels = pr.data.labels.map((label) => label.name) - const status = pr.data.state === "open" ? "open" : pr.data.merged ? "merged" : "closed" - const file_diffs = (await octokit!.rest.pulls.listFiles({ - owner: orgName, - repo: repo, - pull_number: parseInt(prNumber!) - })).data - // console.log(file_diffs) - pulls.push({ - number: parseInt(prNumber!), - repo_name: repoName, - title, - body, - labels, - status, - file_diffs - } as PullRequest) - } + try { + const prURLs = message.match(new RegExp(`https?:\/\/github.com\/${repoName}\/pull\/(?[0-9]+)`, 'gm')); + for (const prURL of prURLs || []) { + const prNumber = prURL.split("/").pop() + const pr = await octokit!.rest.pulls.get({ + owner: orgName, + repo: repo, + pull_number: parseInt(prNumber!) + }) + const title = pr.data.title + const body = pr.data.body + const labels = pr.data.labels.map((label) => label.name) + const status = pr.data.state === "open" ? "open" : pr.data.merged ? "merged" : "closed" + const file_diffs = (await octokit!.rest.pulls.listFiles({ + owner: orgName, + repo: repo, + pull_number: parseInt(prNumber!) + })).data + // console.log(file_diffs) + pulls.push({ + number: parseInt(prNumber!), + repo_name: repoName, + title, + body, + labels, + status, + file_diffs + } as PullRequest) + } - return pulls + return pulls + } catch (e: any) { + toast({ + title: "Failed to retrieve pull request", + description: `The following error has occurred: ${e.message}. Sometimes, logging out and logging back in can resolve this issue.`, + variant: "destructive" + }); + } } function App() { diff --git a/sweep_chat/cypress/e2e/spec.cy.ts b/sweep_chat/cypress/e2e/spec.cy.ts index 08d117064a..912117d166 100644 --- a/sweep_chat/cypress/e2e/spec.cy.ts +++ b/sweep_chat/cypress/e2e/spec.cy.ts @@ -1,4 +1,5 @@ const testMessage = "In the vector search logic, how would I migrate the KNN to use HNSW instead?" +const testPullRequestMessage = "Help me review this PR: https://github.com/sweepai/sweep/pull/3978" describe('sweep chat', () => { beforeEach(() => { @@ -22,13 +23,20 @@ describe('sweep chat', () => { cy.get(':nth-child(5) > .flex').type(testMessage + "{enter}") cy.wait(1000) cy.get(':nth-child(5) > .inline-flex').click() - cy.wait(3000) cy.on('uncaught:exception', (err, runnable) => { expect(err.message).to.include('No snippets found'); return false; }) }) + it("can preview pull requests", () => { + cy.get('.grow > .flex').type("sweepai/sweep").blur() + cy.get(':nth-child(5) > .flex', { timeout: 10000 }).should('have.attr', 'placeholder', 'Type a message...') + + cy.get(':nth-child(5) > .flex').type(testPullRequestMessage + "{enter}") + cy.get('a > .bg-zinc-800').contains('Minor ticket utils fix') + }) + it("can send a message", () => { cy.get('.grow > .flex').type("sweepai/sweep").blur() cy.get(':nth-child(5) > .flex', { timeout: 10000 }).should('have.attr', 'placeholder', 'Type a message...') From ff0632f0c54f092a276ffc51d587a3ffa76af6f3 Mon Sep 17 00:00:00 2001 From: Kevin Lu Date: Wed, 5 Jun 2024 12:07:22 -0700 Subject: [PATCH 2/7] Temporary work --- sweep_chat/components/App.tsx | 109 ++++++++++++++++++++++++++++------ sweep_chat/package-lock.json | 20 +++++++ sweep_chat/package.json | 2 + 3 files changed, 112 insertions(+), 19 deletions(-) diff --git a/sweep_chat/components/App.tsx b/sweep_chat/components/App.tsx index 91c4df76f0..965e48f7e4 100644 --- a/sweep_chat/components/App.tsx +++ b/sweep_chat/components/App.tsx @@ -35,6 +35,7 @@ import PulsingLoader from "./shared/PulsingLoader"; import { Octokit } from "octokit"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "./ui/command"; +import clarinet from "clarinet"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; if (typeof window !== 'undefined') { @@ -457,6 +458,57 @@ const MessageDisplay = ({ message, className, onEdit }: { message: Message, clas ); }; +function getJSONPrefix(buffer: string): [any[], number] { + let stack: string[] = []; + const matchingBrackets: Record = { + '[': ']', + '{': '}', + '(': ')' + }; + var currentIndex = 0; + const results = []; + let inString = false; + let escapeNext = false; + + for (let i = 0; i < buffer.length; i++) { + const char = buffer[i]; + + if (escapeNext) { + escapeNext = false; + continue; + } + + if (char === '\\') { + escapeNext = true; + continue; + } + + if (char === '"') { + inString = !inString; + } + + if (!inString) { + if (matchingBrackets[char]) { + stack.push(char); + } else if (matchingBrackets[stack[stack.length - 1]] === char) { + stack.pop(); + if (stack.length === 0) { + try { + results.push(JSON.parse(buffer.slice(currentIndex, i + 1))); + currentIndex = i + 1; + } catch (e) { + continue; + } + } + } + } + } + if (currentIndex == 0) { + console.log(results, currentIndex, buffer); + } + return [results, currentIndex]; +} + async function* streamMessages( reader: ReadableStreamDefaultReader, isStream: React.MutableRefObject, @@ -482,31 +534,50 @@ async function* streamMessages( done = true; continue; } - + if (value) { const decodedValue = new TextDecoder().decode(value); buffer += decodedValue; - buffer = buffer.replace("][{", "]\n[{"); // Ensure proper JSON formatting for split - buffer = buffer.replace("][[", "]\n[["); // Ensure proper JSON formatting for split - buffer = buffer.replace("]][", "]]\n["); // Ensure proper JSON formatting for split - const bufferLines = buffer.split("\n"); - - for (let line of bufferLines) { // Process all lines except the potentially incomplete last one - if (!line) { - continue - } - try { - const parsedLine = JSON.parse(line); - if (parsedLine) { - yield parsedLine - } - } catch (error) { - console.error("Failed to parse line:", line, error); - } + + const [parsedObjects, currentIndex] = getJSONPrefix(buffer) + for (let parsedObject of parsedObjects) { + console.log(parsedObject) + yield parsedObject } + buffer = buffer.slice(currentIndex) + + // const parsedLine = JSON.parse(buffer); + // if (parsedLine) { + // yield parsedLine; + // } - buffer = bufferLines[bufferLines.length - 1]; // Keep the last line in the buffer + // buffer = ""; } + + // if (value) { + // const decodedValue = new TextDecoder().decode(value); + // buffer += decodedValue; + // buffer = buffer.replace("][{", "]\n[{"); // Ensure proper JSON formatting for split + // buffer = buffer.replace("][[", "]\n[["); // Ensure proper JSON formatting for split + // buffer = buffer.replace("]][", "]]\n["); // Ensure proper JSON formatting for split + // const bufferLines = buffer.split("\n"); + + // for (let line of bufferLines) { // Process all lines except the potentially incomplete last one + // if (!line) { + // continue + // } + // try { + // const parsedLine = JSON.parse(line); + // if (parsedLine) { + // yield parsedLine + // } + // } catch (error) { + // console.error("Failed to parse line:", line, error); + // } + // } + + // buffer = bufferLines[bufferLines.length - 1]; // Keep the last line in the buffer + // } } catch (error) { console.error("Error during streaming:", error); throw error; diff --git a/sweep_chat/package-lock.json b/sweep_chat/package-lock.json index cdd4e07651..f3bc921c5e 100644 --- a/sweep_chat/package-lock.json +++ b/sweep_chat/package-lock.json @@ -19,6 +19,8 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-toast": "^1.1.5", "@sentry/nextjs": "^8.2.1", + "@types/clarinet": "^0.12.3", + "clarinet": "^0.12.6", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -3397,6 +3399,14 @@ "@types/node": "*" } }, + "node_modules/@types/clarinet": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@types/clarinet/-/clarinet-0.12.3.tgz", + "integrity": "sha512-7Fwiv6RNH6JW86U7Vvae7sFHrNmNWxMIx9iAH49F+5d9Kizew5GMYz6rixupn3PK4t+oqP7GZMqOQR77QbcrAQ==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.36", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", @@ -4882,6 +4892,16 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" }, + "node_modules/clarinet": { + "version": "0.12.6", + "resolved": "https://registry.npmjs.org/clarinet/-/clarinet-0.12.6.tgz", + "integrity": "sha512-0FR+TrvLbYHLjhzs9oeIbd3yfZmd4u2DzYQEjUTm2dNfh4Y/9RIRWPjsm3aBtrVEpjKI7+lWa4ouqEXoml84mQ==", + "engines": { + "chrome": ">=16.0.912", + "firefox": ">=0.8.0", + "node": ">=0.3.6" + } + }, "node_modules/class-variance-authority": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", diff --git a/sweep_chat/package.json b/sweep_chat/package.json index 58893cc2f1..d6279efcd0 100644 --- a/sweep_chat/package.json +++ b/sweep_chat/package.json @@ -23,6 +23,8 @@ "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-toast": "^1.1.5", "@sentry/nextjs": "^8.2.1", + "@types/clarinet": "^0.12.3", + "clarinet": "^0.12.6", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", From 39579cfa8a62ead050a84721e7b4369d35d0923d Mon Sep 17 00:00:00 2001 From: Kevin Lu Date: Wed, 5 Jun 2024 16:18:31 -0700 Subject: [PATCH 3/7] Loading bar fixes and autocomplete --- sweep_chat/components/App.tsx | 132 +++++++------- .../components/shared/PulsingLoader.tsx | 7 +- sweep_chat/components/ui/autocomplete.tsx | 165 ++++++++++++++++++ sweep_chat/components/ui/skeleton.tsx | 15 ++ sweep_chat/package-lock.json | 55 ++++-- sweep_chat/package.json | 2 +- 6 files changed, 289 insertions(+), 87 deletions(-) create mode 100644 sweep_chat/components/ui/autocomplete.tsx create mode 100644 sweep_chat/components/ui/skeleton.tsx diff --git a/sweep_chat/components/App.tsx b/sweep_chat/components/App.tsx index 965e48f7e4..7c86f5baa7 100644 --- a/sweep_chat/components/App.tsx +++ b/sweep_chat/components/App.tsx @@ -17,6 +17,7 @@ import { AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; +import { AutoComplete } from "@/components/ui/autocomplete"; import { Toaster } from "@/components/ui/toaster"; import { toast } from "@/components/ui/use-toast"; import { useSession, signIn, SessionProvider, signOut } from "next-auth/react"; @@ -35,7 +36,6 @@ import PulsingLoader from "./shared/PulsingLoader"; import { Octokit } from "octokit"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "./ui/command"; -import clarinet from "clarinet"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; if (typeof window !== 'undefined') { @@ -43,6 +43,8 @@ if (typeof window !== 'undefined') { posthog.debug(false) } +type Repository = any; + interface Snippet { content: string; start: number; @@ -403,7 +405,7 @@ const MessageDisplay = ({ message, className, onEdit }: { message: Message, clas
{!message.function_call!.is_complete ? ( - + ) : ( => { @@ -653,6 +625,8 @@ function App() { const { data: session } = useSession() const posthog = usePostHog(); + const [octokit, setOctokit] = useState(null) + const [repos, setRepos] = useState([]) if (session) { posthog.identify( @@ -663,6 +637,19 @@ function App() { image: session.user!.image, } ); + } else { + return ( +
+ + +
+ ) } useEffect(() => { @@ -676,7 +663,7 @@ function App() { const lastAssistantMessageIndex = messages.findLastIndex((message) => message.role === "assistant" && message.content.trim().length > 0) - const startStream = async (message: string, newMessages: Message[]) => { + const startStream = async (message: string, newMessages: Message[], snippets: Snippet[]) => { setIsLoading(true); isStream.current = true; @@ -737,6 +724,7 @@ function App() { variant: "destructive" }); setIsLoading(false); + isStream.current = false; posthog.capture("chat errored", { repoName, snippets, @@ -824,28 +812,29 @@ function App() { }); } - const [octokit, setOctokit] = useState(null) useEffect(() => { if (session) { const octokit = new Octokit({auth: session.user!.accessToken}) - setOctokit(octokit) + setOctokit(octokit); + (async () => { + const maxPages = 5; + let allRepositories: Repository[] = []; + let page = 1; + let response; + do { + response = await octokit.rest.repos.listForAuthenticatedUser({ + visibility: "all", + sort: "pushed", + per_page: 100, + page: page, + }); + allRepositories = allRepositories.concat(response.data); + setRepos(allRepositories) + page++; + } while (response.data.length !== 0 && page < maxPages); + })() } - }, [session]) - - if (!session) { - return ( -
- - -
- ) - } + }, [session.user!.accessToken]) return (
@@ -881,17 +870,17 @@ function App() {
- setRepoName(e.target.value.replace(/\s/g,''))} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.currentTarget.blur(); - } - }} - onBlur={async () => { + ({label: repo.full_name, value: repo.full_name}))} + placeholder="Repository name" + emptyMessage="No repositories found" + value={{label: repoName, value: repoName}} + onValueChange={(option) => setRepoName(option.value)} + disabled={repoNameDisabled} + onBlur={async (repoName: string) => { + console.log(repoName) const cleanedRepoName = repoName.replace(/\s/g, '') // might be unsafe but we'll handle it once we get there + console.log(repoName) setRepoName(cleanedRepoName) if (cleanedRepoName === "") { setRepoNameValid(false) @@ -943,10 +932,7 @@ function App() { } setRepoNameDisabled(false); }} - placeholder="Repository name" - disabled={repoNameDisabled} /> -
@@ -1065,7 +1053,7 @@ function App() { const newMessages: Message[] = [...messages, { content: currentMessage, role: "user", annotations: { pulls } }]; setMessages(newMessages); setCurrentMessage(""); - startStream(currentMessage, newMessages) + startStream(currentMessage, newMessages, snippets) } }} onChange={(e) => setCurrentMessage(e.target.value)} diff --git a/sweep_chat/components/shared/PulsingLoader.tsx b/sweep_chat/components/shared/PulsingLoader.tsx index 7e8cfc074f..72e2286e89 100644 --- a/sweep_chat/components/shared/PulsingLoader.tsx +++ b/sweep_chat/components/shared/PulsingLoader.tsx @@ -1,9 +1,10 @@ export default function PulsingLoader ({ - size = 8 + size = 2 }: { size: number }) { return ( -
+
) -} \ No newline at end of file +} + diff --git a/sweep_chat/components/ui/autocomplete.tsx b/sweep_chat/components/ui/autocomplete.tsx new file mode 100644 index 0000000000..9183890a1b --- /dev/null +++ b/sweep_chat/components/ui/autocomplete.tsx @@ -0,0 +1,165 @@ +// Source: https://armand-salle.fr/post/autocomplete-select-shadcn-ui/ + +import { + CommandGroup, + CommandItem, + CommandList, + CommandInput, + } from "./command" + import { Command as CommandPrimitive } from "cmdk" + import { useState, useRef, useCallback, type KeyboardEvent } from "react" + + import { Skeleton } from "./skeleton" + + import { Check } from "lucide-react" + import { cn } from "../../lib/utils" + + export type Option = Record<"value" | "label", string> & Record + + type AutoCompleteProps = { + options: Option[] + emptyMessage: string + value?: Option + onValueChange?: (value: Option) => void + isLoading?: boolean + onBlur?: (value: string) => void + disabled?: boolean + placeholder?: string + } + + export const AutoComplete = ({ + options, + placeholder, + emptyMessage, + value, + onValueChange, + onBlur, + disabled = false, + isLoading = false, + }: AutoCompleteProps) => { + const inputRef = useRef(null) + + const [isOpen, setOpen] = useState(false) + const [selected, setSelected] = useState