@@ -54,17 +54,111 @@ export function buildGeminiThread(data: GeminiInterceptedChat): Thread {
5454 } ;
5555}
5656
57- /* ── Extractor stub (actual data comes from content-script) ─ */
57+ /* ── DOM scraper (runs in content-script context) ─────────── */
58+
59+ /**
60+ * Scrape the visible Gemini conversation from the DOM.
61+ * Tries several selector generations – Google reuses build
62+ * hashes so class names change; custom element names are stable.
63+ */
64+ function scrapeGeminiDOM ( ) : GeminiInterceptedChat [ "messages" ] {
65+ const messages : GeminiInterceptedChat [ "messages" ] = [ ] ;
66+
67+ // Candidate query selectors, tried in order.
68+ // Each entry: [userSelector, modelSelector]
69+ const candidatePairs : [ string , string ] [ ] = [
70+ [ "user-query" , "model-response" ] ,
71+ [ "[data-participant-type='user']" , "[data-participant-type='model']" ] ,
72+ [ ".user-query" , ".model-response" ] ,
73+ [ ".conversation-turn[data-turn-type='user']" , ".conversation-turn[data-turn-type='model']" ] ,
74+ ] ;
75+
76+ // Walk conversation turn nodes in DOM order.
77+ // Build a combined list of {role, el} sorted by appearance.
78+ let pairs : { role : "user" | "assistant" ; el : Element } [ ] = [ ] ;
79+
80+ for ( const [ userSel , modelSel ] of candidatePairs ) {
81+ const userEls = Array . from ( document . querySelectorAll ( userSel ) ) ;
82+ const modelEls = Array . from ( document . querySelectorAll ( modelSel ) ) ;
83+ if ( userEls . length > 0 || modelEls . length > 0 ) {
84+ pairs = [
85+ ...userEls . map ( ( el ) => ( { role : "user" as const , el } ) ) ,
86+ ...modelEls . map ( ( el ) => ( { role : "assistant" as const , el } ) ) ,
87+ ] ;
88+ break ;
89+ }
90+ }
91+
92+ if ( pairs . length === 0 ) return messages ;
93+
94+ // Sort by DOM position so turns appear in conversation order.
95+ pairs . sort ( ( a , b ) => {
96+ const pos = a . el . compareDocumentPosition ( b . el ) ;
97+ return pos & Node . DOCUMENT_POSITION_FOLLOWING ? - 1 : 1 ;
98+ } ) ;
99+
100+ for ( const { role, el } of pairs ) {
101+ const text = extractTextFromTurn ( el , role ) ;
102+ if ( text ) messages . push ( { role, text } ) ;
103+ }
104+
105+ return messages ;
106+ }
107+
108+ /** Pull readable text from a single turn element. */
109+ function extractTextFromTurn (
110+ el : Element ,
111+ role : "user" | "assistant"
112+ ) : string {
113+ const userSelectors = [
114+ ".query-text" ,
115+ ".user-query-text" ,
116+ ".prompt-text" ,
117+ ] ;
118+ const modelSelectors = [
119+ ".response-container .markdown" ,
120+ ".response-content .markdown" ,
121+ "message-content" ,
122+ ".model-response-text" ,
123+ ".placeholder.markdown-main-panel" ,
124+ ".markdown" ,
125+ ] ;
126+
127+ const candidates = role === "user" ? userSelectors : modelSelectors ;
128+
129+ for ( const sel of candidates ) {
130+ const child = el . querySelector ( sel ) ;
131+ if ( child ?. textContent ?. trim ( ) ) return child . textContent . trim ( ) ;
132+ }
133+
134+ // Fallback: raw text content of the whole element.
135+ return el . textContent ?. trim ( ) ?? "" ;
136+ }
137+
138+ /* ── Extractor (fetchThread uses DOM scraping) ─────────────── */
58139
59140export const geminiExtractor : Extractor = {
60141 async listConversations ( ) {
61142 // Not feasible from the background – data arrives via content-script intercept
62143 return [ ] ;
63144 } ,
64145
65- async fetchThread ( _conversationId : string ) : Promise < Thread > {
66- throw new Error (
67- "Gemini threads must be intercepted from the content-script. Use GEMINI_INTERCEPTED_CHAT messages."
68- ) ;
146+ async fetchThread ( conversationId : string ) : Promise < Thread > {
147+ const messages = scrapeGeminiDOM ( ) ;
148+
149+ if ( messages . length === 0 ) {
150+ throw new Error (
151+ "No Gemini conversation found on this page. " +
152+ "Make sure the chat has finished loading and try again."
153+ ) ;
154+ }
155+
156+ return buildGeminiThread ( {
157+ conversationId,
158+ title :
159+ document . title . replace ( / [ - – | ] G o o g l e G e m i n i $ / i, "" ) . trim ( ) ||
160+ "Gemini Chat" ,
161+ messages,
162+ } ) ;
69163 } ,
70164} ;
0 commit comments