diff --git a/src/components/PromptList.tsx b/src/components/PromptList.tsx index bbf04450..2e989ded 100644 --- a/src/components/PromptList.tsx +++ b/src/components/PromptList.tsx @@ -1,6 +1,6 @@ import { Link } from "react-router-dom"; import { - extractTitleFromMessage, + parsingPromptText, groupPromptsByRelativeDate, sanitizeQuestionPrompt, } from "@/lib/utils"; @@ -31,15 +31,14 @@ export function PromptList({ prompts }: { prompts: Conversation[] }) { { "font-bold": currentPromptId === prompt.chat_id }, )} > - {extractTitleFromMessage( - prompt.question_answers?.[0]?.question?.message - ? sanitizeQuestionPrompt({ - question: - prompt.question_answers?.[0].question.message, - answer: - prompt.question_answers?.[0]?.answer?.message ?? "", - }) - : `Prompt ${prompt.conversation_timestamp}`, + {parsingPromptText( + sanitizeQuestionPrompt({ + question: + prompt.question_answers?.[0]?.question.message ?? "", + answer: + prompt.question_answers?.[0]?.answer?.message ?? "", + }), + prompt.conversation_timestamp, )} diff --git a/src/components/__tests__/PromptList.test.tsx b/src/components/__tests__/PromptList.test.tsx index 189e90a5..26982578 100644 --- a/src/components/__tests__/PromptList.test.tsx +++ b/src/components/__tests__/PromptList.test.tsx @@ -5,10 +5,52 @@ import mockedPrompts from "@/mocks/msw/fixtures/GET_MESSAGES.json"; import { render } from "@/lib/test-utils"; import { Conversation } from "@/api/generated"; +const conversationTimestamp = "2025-01-02T14:19:58.024100Z"; const prompt = mockedPrompts[0] as Conversation; +const testCases: [string, { message: string; expected: RegExp | string }][] = [ + [ + "codegate cmd", + { + message: "codegate workspace -h", + expected: /codegate workspace -h/i, + }, + ], + [ + "render code with path", + { + message: "// Path: src/lib/utils.ts", + expected: /Prompt on filepath: src\/lib\/utils.ts/i, + }, + ], + [ + "render code with file path", + { + message: " ```tsx // filepath: /tests/my-test.tsx import", + expected: /Prompt on file\/\/ filepath: \/tests\/my-test.tsx/i, + }, + ], + [ + "render snippet", + { + message: + 'Compare this snippet from src/test.ts: // import { fakePkg } from "fake-pkg";', + expected: /Prompt from snippet compare this snippet from src\/test.ts:/i, + }, + ], + [ + "render default", + { + message: + "I know that this local proxy can forward requests to api.foo.com.\n\napi.foo.com will validate whether the connection si trusted using a certificate authority added on the local machine, specifically whether they allow SSL and x.509 basic policy.\n\nI need to be able to validate the proxys ability to make requests to api.foo.com. I only have access to code that can run in the browser. I can infer this based on a successful request. Be creative.", + expected: + "I know that this local proxy can forward requests to api.foo.com. api.foo.com will validate whether the connection si trusted using a certificate authority added on the local machine, specifically whether they allow SSL and x.509 basic policy. I need to be able to validate the proxys ability to make requests to api.foo.com. I only have access to code that can run in the browser. I can infer this based on a successful request. Be creative.", + }, + ], +]; + describe("PromptList", () => { - it("should render correct prompt", () => { + it("render prompt", () => { render(); expect( screen.getByRole("link", { @@ -17,17 +59,26 @@ describe("PromptList", () => { ).toBeVisible(); }); - it("should render default prompt value when missing question", async () => { - const conversationTimestamp = "2025-01-02T14:19:58.024100Z"; + it.each(testCases)("%s", (_title: string, { message, expected }) => { render( , @@ -35,7 +86,7 @@ describe("PromptList", () => { expect( screen.getByRole("link", { - name: `Prompt ${conversationTimestamp}`, + name: expected, }), ).toBeVisible(); }); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index e3f319c0..886881d1 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,14 +1,38 @@ import { AlertConversation, Conversation } from "@/api/generated/types.gen"; import { MaliciousPkgType, TriggerType } from "@/types"; -import { isToday, isYesterday } from "date-fns"; +import { format, isToday, isYesterday } from "date-fns"; const ONE_DAY_MS = 24 * 60 * 60 * 1000; const SEVEN_DAYS_MS = 7 * ONE_DAY_MS; const TEEN_DAYS_MS = 14 * ONE_DAY_MS; const THTY_DAYS_MS = 30 * ONE_DAY_MS; +const FILEPATH_REGEX = /(?:---FILEPATH|Path:|\/\/\s*filepath:)\s*([^\s]+)/g; +const COMPARE_CODE_REGEX = /Compare this snippet[^:]*:/g; -export function extractTitleFromMessage(message: string) { +function parsingByKeys(text: string | undefined, timestamp: string) { + const fallback = `Prompt ${format(new Date(timestamp ?? ""), "y/MM/dd - hh:mm:ss a")}`; try { + if (!text) return fallback; + const filePath = text.match(FILEPATH_REGEX); + const compareCode = text.match(COMPARE_CODE_REGEX); + // there some edge cases in copilot where the prompts are not correctly parsed. In this case is better to show the filepath + if (compareCode || filePath) { + if (filePath) + return `Prompt on file${filePath[0]?.trim().toLocaleLowerCase()}`; + + if (compareCode) + return `Prompt from snippet ${compareCode[0]?.trim().toLocaleLowerCase()}`; + } + + return text.trim(); + } catch { + return fallback; + } +} + +export function parsingPromptText(message: string, timestamp: string) { + try { + // checking malformed markdown code blocks const regex = /^(.*)```[\s\S]*?```(.*)$/s; const match = message.match(regex); @@ -16,10 +40,10 @@ export function extractTitleFromMessage(message: string) { const beforeMarkdown = match[1]?.trim(); const afterMarkdown = match[2]?.trim(); const title = beforeMarkdown || afterMarkdown; - return title; + return parsingByKeys(title, timestamp); } - return message.trim(); + return parsingByKeys(message, timestamp); } catch { return message.trim(); } @@ -119,7 +143,9 @@ export function sanitizeQuestionPrompt({ }) { try { // it shouldn't be possible to receive the prompt answer without a question - if (!answer) return question; + if (!answer) { + throw new Error("Missing AI answer"); + } // Check if 'answer' is truthy; if so, try to find and return the text after "Query:" const index = question.indexOf("Query:"); diff --git a/src/routes/route-chat.tsx b/src/routes/route-chat.tsx index e0c6da82..605d137f 100644 --- a/src/routes/route-chat.tsx +++ b/src/routes/route-chat.tsx @@ -1,6 +1,6 @@ import { useParams } from "react-router-dom"; import { usePromptsData } from "@/hooks/usePromptsData"; -import { extractTitleFromMessage, sanitizeQuestionPrompt } from "@/lib/utils"; +import { parsingPromptText, sanitizeQuestionPrompt } from "@/lib/utils"; import { ChatMessageList } from "@/components/ui/chat/chat-message-list"; import { ChatBubble, @@ -17,22 +17,22 @@ export function RouteChat() { const chat = prompts?.find((prompt) => prompt.chat_id === id); const title = - chat === undefined - ? "" - : extractTitleFromMessage( - chat.question_answers?.[0]?.question?.message - ? sanitizeQuestionPrompt({ - question: chat.question_answers?.[0].question.message, - answer: chat.question_answers?.[0]?.answer?.message ?? "", - }) - : `Prompt ${chat.conversation_timestamp}`, + chat === undefined || + chat.question_answers?.[0]?.question?.message === undefined + ? `Prompt ${id}` + : parsingPromptText( + sanitizeQuestionPrompt({ + question: chat.question_answers?.[0].question.message, + answer: chat.question_answers?.[0]?.answer?.message ?? "", + }), + chat.conversation_timestamp, ); return ( <> - {title} + {title}