Skip to content

Commit c56f22f

Browse files
committed
feat: claude support
1 parent 5e0faef commit c56f22f

File tree

13 files changed

+458
-25
lines changed

13 files changed

+458
-25
lines changed

src/api/getConversation.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { getConversation as chatgptGetConversation } from "~models/chatgpt/api/getConversation"
2+
import { getConversation as claudeGetConversation } from "~models/claude/api/getConversation"
3+
import { getConversation as deepseekGetConversation } from "~models/deepseek/api/getConversation"
4+
import { getConversation as mistralGetConversation } from "~models/mistral/api/getConversation"
25
import type { SupportedModels } from "~utils/types"
36

4-
import { getConversation as deepseekGetConversation } from "../models/deepseek/api/getConversation"
5-
import { getConversation as mistralGetConversation } from "./mistral/getConversation"
6-
77
export const getConversation = async ({ model, params }: Params) => {
88
switch (model) {
99
case "chatgpt":
@@ -12,6 +12,8 @@ export const getConversation = async ({ model, params }: Params) => {
1212
return deepseekGetConversation(params)
1313
case "mistral":
1414
return mistralGetConversation(params)
15+
case "claude":
16+
return claudeGetConversation(params)
1517
default:
1618
throw new Error("Model not supported")
1719
}

src/background/functions/save.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ const save = async (
4949

5050
console.log({ rawHeaders })
5151
const headers = convertHeaders(
52-
rawHeaders.filter((h) => h.name.toLowerCase() != "cookie")
52+
// rawHeaders.filter((h) => h.name.toLowerCase() != "cookie")
53+
rawHeaders
5354
)
5455
const rawConversation = await getConversation({
5556
model: model,
@@ -65,7 +66,11 @@ const save = async (
6566
textDocs =
6667
(await getConversationTextdocs({
6768
model: model as any,
68-
params: { rawConversation, headers, includeVersions: true }
69+
params: {
70+
rawConversation,
71+
headers,
72+
includeVersions: true
73+
}
6974
})) ?? []
7075
}
7176

src/background/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ const saveHeaders = (model: SupportedModels, headers: any) => {
7878
const trackedURLs = [
7979
"https://chatgpt.com/*",
8080
"https://chat.deepseek.com/*",
81-
"https://chat.mistral.ai/*"
81+
"https://chat.mistral.ai/*",
82+
"https://claude.ai/*"
8283
]
8384

8485
const deepseekUrls = [
@@ -124,6 +125,14 @@ chrome.webRequest.onSendHeaders.addListener(
124125
return
125126
}
126127

128+
if (
129+
res.requestHeaders.some((h) => h.name.toLowerCase() === "cookie") &&
130+
res.url.includes("claude.ai")
131+
) {
132+
saveHeaders("claude", res.requestHeaders)
133+
return
134+
}
135+
127136
// cacheHeaders = res.requestHeaders
128137
},
129138
{

src/background/messages/getCurrentTab.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
3131
model = "mistral"
3232
await storage.set(STORAGE_KEYS.model, "mistral")
3333
}
34+
if (tab.url?.includes("claude.ai")) {
35+
model = "claude"
36+
await storage.set(STORAGE_KEYS.model, "claude")
37+
}
3438

3539
res.send({ tabId: tab.id, tabUrl: tab.url, model })
3640
} catch (err) {

src/contents/claude.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {
2+
Content,
3+
config as _config,
4+
getInlineAnchorList as _getInlineAnchorList,
5+
render as _render
6+
} from "~models/claude/contents"
7+
8+
export const config = _config
9+
export const getInlineAnchorList = _getInlineAnchorList
10+
export const render = _render
11+
12+
export default Content

src/contents/popup.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export const config: PlasmoCSConfig = {
1616
matches: [
1717
"https://chat.deepseek.com/*",
1818
"https://chatgpt.com/*",
19-
"https://chat.mistral.ai/*"
19+
"https://chat.mistral.ai/*",
20+
"https://claude.ai/*"
2021
]
2122
}
2223

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Conversation } from "~utils/types"
2+
3+
export const getConversation = async ({
4+
convId,
5+
headers
6+
}: {
7+
convId: string
8+
headers: any
9+
}) => {
10+
try {
11+
const tabs = await chrome.tabs.query({
12+
active: true,
13+
currentWindow: true
14+
})
15+
16+
const tab = tabs[0]
17+
if (!tab.id) throw new Error("No active tab found")
18+
19+
const pattern = /(?<=lastActiveOrg=)([^;]+)/
20+
21+
const match = headers["Cookie"].match(pattern)
22+
23+
const orgId = match[0]
24+
25+
const req = await chrome.scripting.executeScript({
26+
target: { tabId: tab.id },
27+
func: (headers, convId, orgId) =>
28+
fetch(
29+
`https://claude.ai/api/organizations/${orgId}/chat_conversations/${convId}?tree=True&rendering_mode=messages&render_all_tools=true`,
30+
{
31+
method: "GET",
32+
headers: headers,
33+
referrer: "https://claude.ai/chat/" + convId,
34+
referrerPolicy: "strict-origin-when-cross-origin",
35+
mode: "cors",
36+
credentials: "include"
37+
}
38+
).then((res) => res.json()),
39+
args: [headers, convId, orgId]
40+
})
41+
42+
const data = req[0].result
43+
44+
console.log(data)
45+
46+
return data as Conversation["claude"]
47+
} catch (err) {
48+
console.error(err)
49+
50+
return null
51+
}
52+
}

src/models/claude/contents/index.tsx

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import type {
2+
PlasmoCSConfig,
3+
PlasmoGetInlineAnchorList,
4+
PlasmoRender
5+
} from "plasmo"
6+
import { useCallback, useEffect, useState } from "react"
7+
import { createRoot } from "react-dom/client"
8+
import { compress } from "shrink-string"
9+
10+
import { useStorage } from "@plasmohq/storage/hook"
11+
12+
import LogoIcon from "~common/logo"
13+
import PinIcon from "~common/pin"
14+
15+
// import ClaudeLogoIcon from "~common/claudeLogo"
16+
import "~styles.css"
17+
18+
import { STORAGE_KEYS } from "~utils/consts"
19+
import { getChatConfig, i18n } from "~utils/functions"
20+
import type { AutosaveStatus, PopupEnum, ToBeSaved } from "~utils/types"
21+
22+
// Run on Claude conversation pages
23+
export const config: PlasmoCSConfig = {
24+
matches: ["https://claude.ai/*"]
25+
}
26+
27+
// Select each Claude message container (adjust selector as needed)
28+
export const getInlineAnchorList: PlasmoGetInlineAnchorList = async () =>
29+
document.querySelectorAll(".font-claude-message")
30+
31+
export const render: PlasmoRender<Element> = async ({
32+
anchor,
33+
createRootContainer
34+
}) => {
35+
if (!anchor || !createRootContainer) return
36+
37+
const rootContainer = await createRootContainer(anchor)
38+
const root = createRoot(rootContainer)
39+
const parent = anchor.element
40+
41+
// Mark this element for pin indexing
42+
parent?.classList.add("pin")
43+
44+
root.render(<Content parent={parent} />)
45+
}
46+
47+
type Props = { parent: Element }
48+
49+
export const Content = ({ parent }: Props) => {
50+
const [toBeSaved, setToBeSaved] = useStorage<ToBeSaved>(
51+
STORAGE_KEYS.toBeSaved
52+
)
53+
const [showPopup, setShowPopup] = useStorage<PopupEnum | false>(
54+
STORAGE_KEYS.showPopup,
55+
false
56+
)
57+
const [authenticated] = useStorage(STORAGE_KEYS.authenticated, false)
58+
const [isPremium] = useStorage(STORAGE_KEYS.isPremium, false)
59+
const [activeTrial] = useStorage(STORAGE_KEYS.activeTrial, false)
60+
const [chatID] = useStorage(STORAGE_KEYS.chatID, "")
61+
const [status] = useStorage<AutosaveStatus>(
62+
STORAGE_KEYS.autosaveStatus,
63+
"generating"
64+
)
65+
66+
const [autosaveEnabled, setAutosave] = useState(false)
67+
const [isLastMessage, setIsLastMessage] = useState(false)
68+
const [showPin, setShowPin] = useState(true)
69+
const [pinIndex, setPinIndex] = useState(-1)
70+
71+
useEffect(() => {
72+
setPinIndex(getPinIndex(parent))
73+
}, [parent])
74+
75+
useEffect(() => {
76+
if (!(isPremium || activeTrial) || !chatID) return
77+
const checkAutosave = async () => {
78+
const config = await getChatConfig(chatID)
79+
if (config) setAutosave(config.enabled)
80+
}
81+
checkAutosave()
82+
83+
setIsLastMessage(() => {
84+
const pins = document.querySelectorAll(".pin")
85+
const lastPin = pins[pins.length - 1]
86+
if (!lastPin) return false
87+
const parentNode = parent.firstChild?.parentNode
88+
return parentNode
89+
? lastPin.firstChild?.parentNode?.isSameNode(
90+
parent.firstChild?.parentNode
91+
) ?? false
92+
: false
93+
})
94+
}, [chatID, status, parent, isPremium, activeTrial])
95+
96+
const LastMessageIcon = useCallback(() => {
97+
switch (status) {
98+
case "disabled":
99+
return <LogoIcon />
100+
case "generating":
101+
return <LogoIcon loading />
102+
case "saving":
103+
return <LogoIcon loading />
104+
case "error":
105+
return <LogoIcon error />
106+
case "saved":
107+
return <LogoIcon />
108+
default:
109+
return <LogoIcon />
110+
}
111+
}, [status])
112+
113+
const handleClick = async () => {
114+
if (!authenticated) {
115+
alert(i18n("errConnect"))
116+
return
117+
}
118+
119+
const container = parent.parentElement
120+
console.log({ parent, container })
121+
122+
// For Claude, assume message text is in <p> elements
123+
const text = Array.from(parent!.querySelectorAll(".grid")).map(
124+
(el) => el.innerHTML
125+
)
126+
127+
let uncompressedAnswer = text[0]
128+
129+
const answer = await compress(uncompressedAnswer)
130+
131+
// Assume prompt text is in a <p> within the previous sibling container
132+
const promptElement =
133+
container?.parentElement?.parentElement?.previousElementSibling?.querySelector(
134+
"p.whitespace-pre-wrap"
135+
)
136+
const prompt = promptElement
137+
? await compress(promptElement.textContent || "")
138+
: ""
139+
const title = document.title
140+
const url = window.location.href
141+
142+
await setToBeSaved({ answer, prompt, title, url, pin: pinIndex })
143+
await setShowPopup("save")
144+
}
145+
146+
if (!showPin) return null
147+
148+
// If autosave is enabled, render with adjusted styling
149+
if (autosaveEnabled)
150+
return (
151+
<div style={{ position: "relative", width: "100%", height: 33 }}>
152+
<button
153+
className="flex items-center ml-2 mt-1"
154+
onClick={
155+
isLastMessage && status === "error"
156+
? () => setShowPopup("error")
157+
: status === "saved"
158+
? handleClick
159+
: undefined
160+
}
161+
style={{
162+
position: "absolute",
163+
right: 0,
164+
background: "transparent",
165+
border: "none",
166+
marginTop: 10,
167+
cursor: status !== "generating" ? "pointer" : "default"
168+
}}>
169+
{isLastMessage ? <LastMessageIcon /> : <LogoIcon />}
170+
</button>
171+
</div>
172+
)
173+
174+
return (
175+
<div style={{ position: "relative", width: "100%", height: 33 }}>
176+
<button
177+
onClick={handleClick}
178+
className="text-gray-800 dark:text-gray-100 flex items-center ml-2 mt-1"
179+
style={{
180+
position: "absolute",
181+
right: 0,
182+
background: "transparent",
183+
border: "none",
184+
marginTop: 10,
185+
padding: 4,
186+
borderRadius: 4,
187+
cursor: "pointer"
188+
}}>
189+
<PinIcon />
190+
</button>
191+
</div>
192+
)
193+
}
194+
195+
const getPinIndex = (parent: Element) => {
196+
const pins = document.querySelectorAll(".pin")
197+
return Array.from(pins).findIndex((pin) => pin.isSameNode(parent))
198+
}

0 commit comments

Comments
 (0)