Skip to content
Open
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
13 changes: 12 additions & 1 deletion apps/browser-extension/src/hooks/use-copy-conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function useCopyConversation() {
setResult(null);
}, 1500);
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
const message = getErrorMessage(err);
setError(message);
setState("error");

Expand All @@ -60,3 +60,14 @@ export function useCopyConversation() {

return { state, result, error, copy };
}

// Plugins throw plain AppError objects ({ code, message, detail }), not Error instances
function getErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
if (err && typeof err === "object") {
const { message, detail } = err as { message?: unknown; detail?: unknown };
if (typeof detail === "string" && detail) return detail;
if (typeof message === "string" && message) return message;
}
return "Unknown error";
}
85 changes: 85 additions & 0 deletions packages/core-plugins/src/plugins/chatgpt/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import type { PluginContext } from "../../../types";
import { chatgptPlugin } from "../plugin";

const CONVERSATION_ID = "68ab12cd-3456-7890-abcd-ef0123456789";

function mockChatGPTApi() {
const fetchMock = vi.fn(async (input: string) => {
if (input.endsWith("/api/auth/session")) {
return new Response(JSON.stringify({ accessToken: "token" }));
}
return new Response(
JSON.stringify({
title: "Test",
current_node: "a",
mapping: {
u: {
id: "u",
parent: null,
children: ["a"],
message: {
author: { role: "user" },
content: { content_type: "text", parts: ["Hello"] },
},
},
a: {
id: "a",
parent: "u",
children: [],
message: {
author: { role: "assistant" },
content: { content_type: "text", parts: ["Hi"] },
},
},
},
}),
);
});
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
}

function ctx(url: string): PluginContext {
return { url, document: {} as Document };
}

describe("chatgptPlugin.extract", () => {
beforeEach(() => {
vi.unstubAllGlobals();
});

it("extracts a regular conversation", async () => {
const fetchMock = mockChatGPTApi();
const bundle = await chatgptPlugin.extract(
ctx(`https://chatgpt.com/c/${CONVERSATION_ID}`),
);
expect(bundle.nodes).toHaveLength(2);
expect(fetchMock).toHaveBeenCalledWith(
`https://chatgpt.com/backend-api/conversation/${CONVERSATION_ID}`,
expect.anything(),
);
});

it("extracts a conversation inside a project", async () => {
const fetchMock = mockChatGPTApi();
const bundle = await chatgptPlugin.extract(
ctx(
`https://chatgpt.com/g/g-p-67f0a1b2c3d4-vosstanovlenie-saita/c/${CONVERSATION_ID}`,
),
);
expect(bundle.nodes).toHaveLength(2);
expect(fetchMock).toHaveBeenCalledWith(
`https://chatgpt.com/backend-api/conversation/${CONVERSATION_ID}`,
expect.anything(),
);
});

it("rejects non-conversation pages", async () => {
await expect(
chatgptPlugin.extract(
ctx("https://chatgpt.com/g/g-p-67f0a1b2c3d4/project"),
),
).rejects.toMatchObject({ code: "E-PARSE-001" });
});
});
3 changes: 2 additions & 1 deletion packages/core-plugins/src/plugins/chatgpt/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import { buildLinearConversation } from "./tree-linearizer";
import type { ChatGPTConversationResponse, MessageNode } from "./types";

const HOST_PATTERN = /^https:\/\/(?:chatgpt\.com|chat\.openai\.com)\//i;
// Matches both /c/<id> and project conversations at /g/<project>/c/<id>
const CONVERSATION_PATTERN =
/^https?:\/\/(?:chat\.openai\.com|chatgpt\.com)\/c\/([a-zA-Z0-9-]+)/;
/^https?:\/\/(?:chat\.openai\.com|chatgpt\.com)\/(?:g\/[^/]+\/)?c\/([a-zA-Z0-9-]+)/;

const SESSION_ENDPOINT = "https://chatgpt.com/api/auth/session";
const API_ENDPOINT = "https://chatgpt.com/backend-api/conversation";
Expand Down
Loading