Skip to content

Commit ee58ce0

Browse files
committed
feat: export entire thread
1 parent fe1db28 commit ee58ce0

File tree

7 files changed

+144
-8
lines changed

7 files changed

+144
-8
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React, {
2+
ReactNode,
3+
useCallback,
4+
useMemo,
5+
useState,
6+
} from 'react';
7+
import ReactDOM from 'react-dom';
8+
9+
import $ from 'jquery';
10+
import {
11+
Check,
12+
Download,
13+
LoaderCircle,
14+
} from 'lucide-react';
15+
16+
import { ThreadInfoAPIResponse } from '@/types/PPLXApi';
17+
import { ui } from '@/utils/ui';
18+
import {
19+
jsonUtils,
20+
whereAmI,
21+
} from '@/utils/utils';
22+
import { useQuery } from '@tanstack/react-query';
23+
24+
import useElementObserver from './hooks/useElementObserver';
25+
import { Button } from './ui/button';
26+
27+
export default function ThreadExportButton() {
28+
const { refetch, isFetching: isFetchingCurrentThreadInfo } = useQuery<
29+
ThreadInfoAPIResponse[]
30+
>({
31+
queryKey: ['currentThreadInfo'],
32+
enabled: false,
33+
});
34+
35+
const [container, setContainer] = React.useState<Element>();
36+
37+
const idleSaveButtonText = useMemo(
38+
() => (
39+
<>
40+
<Download className="tw-mr-2 tw-w-4 tw-h-4" />
41+
<span>Export</span>
42+
</>
43+
),
44+
[]
45+
);
46+
47+
const [saveButtonText, setSaveButtonText] =
48+
useState<ReactNode>(idleSaveButtonText);
49+
50+
useElementObserver({
51+
selector: () => [ui.getStickyHeader()[0]],
52+
callback: ({ element }) => {
53+
const myContainer = $('<div>');
54+
55+
$(element).find('>div>div:last>div:last').before(myContainer);
56+
57+
setContainer(myContainer[0]);
58+
},
59+
observedIdentifier: 'thread-export-button',
60+
});
61+
62+
const handleExportThread = useCallback(async () => {
63+
const result = await refetch();
64+
65+
if (!result.data) return;
66+
67+
let outputText = '';
68+
69+
result.data?.map((message) => {
70+
outputText += `Question: ${message.query_str}\n\n`;
71+
72+
const answer =
73+
jsonUtils.safeParse(message.text)?.answer ||
74+
jsonUtils.safeParse(
75+
jsonUtils.safeParse(message.text)?.[4].content.answer
76+
)?.answer;
77+
78+
outputText += `Answer: ${answer}\n\n`;
79+
80+
const proSearchWebResults = jsonUtils.safeParse(message.text)?.[2]
81+
?.content.web_results;
82+
const normalSearchWebResults = jsonUtils.safeParse(
83+
message.text
84+
).web_results;
85+
86+
(proSearchWebResults || normalSearchWebResults).map(
87+
(
88+
webResult: {
89+
name: string;
90+
url: string;
91+
},
92+
index: number
93+
) => {
94+
outputText += `[${index + 1}] [${webResult.name}](${webResult.url}) \n`;
95+
}
96+
);
97+
98+
outputText += '\n---\n\n';
99+
});
100+
101+
navigator.clipboard.writeText(outputText);
102+
103+
setSaveButtonText(
104+
<>
105+
<Check className="tw-mr-2 tw-w-4 tw-h-4" />
106+
<span>Copied</span>
107+
</>
108+
);
109+
110+
setTimeout(() => {
111+
setSaveButtonText(idleSaveButtonText);
112+
}, 2000);
113+
}, [refetch, idleSaveButtonText]);
114+
115+
if (whereAmI() !== 'thread' || !container) return null;
116+
117+
return ReactDOM.createPortal(
118+
<Button
119+
className="tw-h-[2rem] tw-text-muted-foreground hover:tw-text-foreground !tw-p-2"
120+
variant="outline"
121+
onClick={() => handleExportThread()}
122+
disabled={isFetchingCurrentThreadInfo}
123+
>
124+
{isFetchingCurrentThreadInfo ? (
125+
<LoaderCircle className="tw-w-4 tw-h-4 tw-animate-spin" />
126+
) : (
127+
saveButtonText
128+
)}
129+
</Button>,
130+
container
131+
);
132+
}

src/components/ThreadQueryFormatSwitch.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export default function ThreadQueryFormatSwitch() {
2828

2929
useEffect(() => {
3030
containers.forEach((container, index) => {
31-
if ($(container.query).find('#markdown-query-wrapper.tw-hidden').length) {
31+
if ($(container.query).find('#markdown-query-wrapper.\\!tw-hidden').length) {
3232
setIsMarkdown((draft) => {
3333
draft[index] = false;
3434
});
@@ -74,10 +74,10 @@ export default function ThreadQueryFormatSwitch() {
7474
if (whereAmI() !== 'thread') return;
7575

7676
const isMarkdown =
77-
$(element).parent().find('#markdown-query-wrapper:not(.tw-hidden)')
77+
$(element).parent().find('#markdown-query-wrapper:not(.\\!tw-hidden)')
7878
.length > 0 || !$(element).parent().find('#markdown-query-wrapper').length;
7979

80-
$(element).toggleClass('tw-hidden', isMarkdown);
80+
$(element).toggleClass('!tw-hidden', isMarkdown);
8181
},
8282
observedIdentifier: 'append-tw-block-to-plain-text-block',
8383
});
@@ -92,10 +92,10 @@ export default function ThreadQueryFormatSwitch() {
9292
setIsMarkdown((draft) => {
9393
$(container.query)
9494
.find('.whitespace-pre-line.break-words')
95-
.toggleClass('tw-hidden', !draft[index]);
95+
.toggleClass('!tw-hidden', !draft[index]);
9696
$(container.query)
9797
.find('#markdown-query-wrapper')
98-
.toggleClass('tw-hidden', draft[index]);
98+
.toggleClass('!tw-hidden', draft[index]);
9999
draft[index] = !draft[index];
100100
});
101101
}}

src/components/hooks/useQuickQueryCommanderParams.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ export default function useQuickQueryCommanderParams({
4949
});
5050

5151
const { data: currentThreadInfo, isFetching: isFetchingCurrentThreadInfo } =
52-
useQuery<ThreadInfoAPIResponse>({
52+
useQuery<ThreadInfoAPIResponse[], Error, ThreadInfoAPIResponse>({
5353
queryKey: ['currentThreadInfo'],
5454
enabled: false,
55+
select: (data) => data?.[0],
5556
});
5657

5758
const quickQueryParams = useMemo(() => {

src/content-script/Root.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import useElementObserver from '@/components/hooks/useElementObserver';
66
import MainPage from '@/components/MainPage';
77
import QueryBox from '@/components/QueryBox';
88
import ThreadAnchor from '@/components/ThreadAnchor';
9+
import ThreadExportButton from '@/components/ThreadExportButton';
910
import ThreadQueryFormatSwitch from '@/components/ThreadQueryFormatSwitch';
1011
import { Toaster } from '@/components/ui/toaster';
1112
import { useToast } from '@/components/ui/use-toast';
@@ -34,6 +35,7 @@ export default function Root() {
3435
<Commander />
3536
{popupSettingsStore.getState().qolTweaks.threadTOC && <ThreadAnchor />}
3637
{popupSettingsStore.getState().visualTweaks.threadQueryMarkdown && <ThreadQueryFormatSwitch />}
38+
<ThreadExportButton />
3739
<Toaster />
3840
<IncompatibleInterfaceLanguageNotice />
3941
<ReactQueryDevtools initialIsOpen={false} />

src/popup/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function UntrustedBuildWarning() {
5353
<div className="tw-w-full tw-bg-secondary tw-flex tw-flex-col tw-font-sans">
5454
<Separator />
5555
<div className="tw-flex tw-px-2 tw-py-2 tw-text-sm tw-font-bold tw-gap-1 tw-bg-destructive tw-items-center">
56-
<CircleAlert className="tw-h-3 tw-w-3 tw-mr-1 tw-text-accent-foreground" />
56+
<CircleAlert className="tw-h-8 tw-w-8 tw-mr-1 tw-text-accent-foreground" />
5757
<span>This is an untrusted build. Use at your own risk.</span>
5858
</div>
5959
</div>

src/types/PPLXApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type CollectionsAPIResponse = {
2525
}[];
2626

2727
export type ThreadInfoAPIResponse = {
28+
query_str: string;
2829
text: string;
2930
backend_uuid: string;
3031
author_image: string;

src/utils/pplx-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ async function fetchThreadInfo(threadSlug: string) {
102102
if (!data) return null;
103103

104104
return data.pageProps.dehydratedState.queries[1].state
105-
.data[0] as ThreadInfoAPIResponse;
105+
.data as ThreadInfoAPIResponse[];
106106
}
107107

108108
async function fetchUserProfileSettings(): Promise<UserProfileSettingsAPIResponse | null> {

0 commit comments

Comments
 (0)