mirror of
https://github.com/pionxzh/chatgpt-exporter.git
synced 2026-07-23 09:00:51 -05:00
@@ -27,6 +27,7 @@
|
||||
"eventemitter3": "^5.0.0",
|
||||
"hast-util-to-html": "^8.0.4",
|
||||
"html2canvas": "^1.4.1",
|
||||
"i18next": "^22.4.14",
|
||||
"jszip": "3.9.1",
|
||||
"mdast": "^3.0.0",
|
||||
"mdast-util-from-markdown": "^1.3.0",
|
||||
@@ -36,6 +37,7 @@
|
||||
"mdast-util-to-markdown": "^1.5.0",
|
||||
"micromark-extension-gfm": "^2.0.1",
|
||||
"preact": "^10.13.2",
|
||||
"react-i18next": "^12.2.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sentinel-js": "^0.0.5",
|
||||
"urlcat": "^2.0.4",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export const baseUrl = 'https://chat.openai.com'
|
||||
|
||||
export const LEGACY_KEY_FILENAME_FORMAT = 'exporter-format'
|
||||
export const KEY_LANGUAGE = 'exporter:language'
|
||||
export const KEY_FILENAME_FORMAT = 'exporter:filename_format'
|
||||
export const KEY_TIMESTAMP_ENABLED = 'exporter:enable_timestamp'
|
||||
export const KEY_TIMESTAMP_24H = 'exporter:timestamp_24h'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import JSZip from 'jszip'
|
||||
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
|
||||
import { KEY_TIMESTAMP_24H, KEY_TIMESTAMP_ENABLED, baseUrl } from '../constants'
|
||||
import i18n from '../i18n'
|
||||
import { checkIfConversationStarted, getConversationChoice, getUserAvatar } from '../page'
|
||||
import templateHtml from '../template.html?raw'
|
||||
import { downloadFile, getFileNameWithFormat } from '../utils/download'
|
||||
@@ -13,7 +14,7 @@ import type { ExportMeta } from '../ui/SettingContext'
|
||||
|
||||
export async function exportToHtml(fileNameFormat: string, metaList: ExportMeta[]) {
|
||||
if (!checkIfConversationStarted()) {
|
||||
alert('Please start a conversation first.')
|
||||
alert(i18n.t('Please start a conversation first'))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import html2canvas from 'html2canvas'
|
||||
import i18n from '../i18n'
|
||||
import { checkIfConversationStarted, getChatIdFromUrl } from '../page'
|
||||
import { downloadUrl, getFileNameWithFormat } from '../utils/download'
|
||||
import { Effect } from '../utils/effect'
|
||||
@@ -11,7 +12,7 @@ function fnIgnoreElements(el: any) {
|
||||
|
||||
export async function exportToPng(fileNameFormat: string) {
|
||||
if (!checkIfConversationStarted()) {
|
||||
alert('Please start a conversation first.')
|
||||
alert(i18n.t('Please start a conversation first'))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import JSZip from 'jszip'
|
||||
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
|
||||
import i18n from '../i18n'
|
||||
import { checkIfConversationStarted, getConversationChoice } from '../page'
|
||||
import { downloadFile, getFileNameWithFormat } from '../utils/download'
|
||||
import type { ApiConversationWithId } from '../api'
|
||||
|
||||
export async function exportToJson(fileNameFormat: string) {
|
||||
if (!checkIfConversationStarted()) {
|
||||
alert('Please start a conversation first.')
|
||||
alert(i18n.t('Please start a conversation first'))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import JSZip from 'jszip'
|
||||
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
|
||||
import { baseUrl } from '../constants'
|
||||
import i18n from '../i18n'
|
||||
import { checkIfConversationStarted, getConversationChoice } from '../page'
|
||||
import { downloadFile, getFileNameWithFormat } from '../utils/download'
|
||||
import { fromMarkdown, toMarkdown } from '../utils/markdown'
|
||||
@@ -11,7 +12,7 @@ import type { ExportMeta } from '../ui/SettingContext'
|
||||
|
||||
export async function exportToMarkdown(fileNameFormat: string, metaList: ExportMeta[]) {
|
||||
if (!checkIfConversationStarted()) {
|
||||
alert('Please start a conversation first.')
|
||||
alert(i18n.t('Please start a conversation first'))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { fetchConversation, getCurrentChatId, processConversation } from '../api'
|
||||
import i18n from '../i18n'
|
||||
import { checkIfConversationStarted, getConversationChoice } from '../page'
|
||||
import { copyToClipboard } from '../utils/clipboard'
|
||||
import { flatMap, fromMarkdown, toMarkdown } from '../utils/markdown'
|
||||
@@ -7,7 +8,7 @@ import type { Emphasis, Strong } from 'mdast'
|
||||
|
||||
export async function exportToText() {
|
||||
if (!checkIfConversationStarted()) {
|
||||
alert('Please start a conversation first.')
|
||||
alert(i18n.t('Please start a conversation first'))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -43,7 +44,7 @@ export async function exportToText() {
|
||||
|
||||
export async function exportToTextFromIndex(index: number) {
|
||||
if (!checkIfConversationStarted()) {
|
||||
alert('Please start a conversation first.')
|
||||
alert(i18n.t('Please start a conversation first'))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
124
packages/userscript/src/i18n.ts
Normal file
124
packages/userscript/src/i18n.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import { KEY_LANGUAGE } from './constants'
|
||||
import en_US from './locales/en.json'
|
||||
import ja_JP from './locales/jp.json'
|
||||
import zh_Hans from './locales/zh-Hans.json'
|
||||
import zh_Hant from './locales/zh-Hant.json'
|
||||
import { ScriptStorage } from './utils/storage'
|
||||
|
||||
declare module 'i18next' {
|
||||
// Refs: https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz
|
||||
interface CustomTypeOptions {
|
||||
returnNull: false
|
||||
}
|
||||
}
|
||||
|
||||
interface Locale {
|
||||
name: string
|
||||
code: string
|
||||
aliases?: string[]
|
||||
resource: Record<string, string>
|
||||
}
|
||||
|
||||
const EN_US = {
|
||||
name: 'English',
|
||||
code: 'en-US',
|
||||
resource: en_US,
|
||||
}
|
||||
|
||||
const JA_JP = {
|
||||
name: '日本語',
|
||||
code: 'ja-JP',
|
||||
resource: ja_JP,
|
||||
}
|
||||
|
||||
const ZH_Hans = {
|
||||
name: '简体中文',
|
||||
code: 'zh-Hans',
|
||||
resource: zh_Hans,
|
||||
}
|
||||
|
||||
const ZH_Hant = {
|
||||
name: '繁體中文',
|
||||
code: 'zh-Hant',
|
||||
resource: zh_Hant,
|
||||
}
|
||||
|
||||
export const LOCALES: Locale[] = [
|
||||
EN_US,
|
||||
JA_JP,
|
||||
ZH_Hans,
|
||||
ZH_Hant,
|
||||
]
|
||||
|
||||
const LanguageMapping: Record<string, string> = {
|
||||
'en': EN_US.code,
|
||||
'en-US': EN_US.code,
|
||||
|
||||
'ja': JA_JP.code,
|
||||
'ja-JP': JA_JP.code,
|
||||
|
||||
'zh': ZH_Hans.code,
|
||||
'zh-CN': ZH_Hans.code,
|
||||
'zh-SG': ZH_Hans.code,
|
||||
'zh-Hans': ZH_Hans.code,
|
||||
|
||||
'zh-HK': ZH_Hant.code,
|
||||
'zh-MO': ZH_Hant.code,
|
||||
'zh-TW': ZH_Hant.code,
|
||||
'zh-Hant': ZH_Hant.code,
|
||||
}
|
||||
|
||||
const resources = LOCALES.reduce<Record<string, { translation: Record<string, string> }>>((acc, cur) => {
|
||||
acc[cur.code] = { translation: cur.resource }
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const standardizeLanguage = (language: string) => {
|
||||
if (language in LanguageMapping) return LanguageMapping[language]
|
||||
|
||||
const shortLang = language.split('-')[0]
|
||||
if (shortLang in LanguageMapping) return LanguageMapping[shortLang]
|
||||
|
||||
return language
|
||||
}
|
||||
|
||||
const getNavigatorLanguage = () => {
|
||||
const { language, languages } = navigator
|
||||
if (language) return language
|
||||
|
||||
if (languages && languages.length) {
|
||||
return languages[0]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const getDefaultLanguage = () => {
|
||||
const storedLanguage = ScriptStorage.get<string>(KEY_LANGUAGE)
|
||||
if (storedLanguage) return standardizeLanguage(storedLanguage)
|
||||
|
||||
const browserLanguage = getNavigatorLanguage()
|
||||
if (browserLanguage) return standardizeLanguage(browserLanguage)
|
||||
|
||||
return EN_US.code
|
||||
}
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: EN_US.code,
|
||||
lng: getDefaultLanguage(),
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
resources,
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
})
|
||||
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
ScriptStorage.set(KEY_LANGUAGE, lng)
|
||||
})
|
||||
|
||||
export default i18n
|
||||
32
packages/userscript/src/locales/en.json
Normal file
32
packages/userscript/src/locales/en.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"title": "ChatGPT Exporter",
|
||||
"ExportHelper": "Export",
|
||||
"Setting": "Setting",
|
||||
"Language": "Language",
|
||||
"Copy Text": "Copy Text",
|
||||
"Copied!": "Copied!",
|
||||
"Screenshot": "Screenshot",
|
||||
"Markdown": "Markdown",
|
||||
"HTML": "HTML",
|
||||
"JSON": "JSON",
|
||||
"Save": "Save",
|
||||
"Delete": "Delete",
|
||||
"Select All": "Select All",
|
||||
"Export": "Export",
|
||||
"Error": "Error",
|
||||
"Loading": "Loading",
|
||||
"Preview": "Preview",
|
||||
"File Name": "File Name",
|
||||
"Export All": "Export All",
|
||||
"Exporter Settings": "Exporter Settings",
|
||||
"Export Dialog Title": "Export Conversations",
|
||||
"Available variables": "Available variables",
|
||||
"Conversation Timestamp": "Conversation Timestamp",
|
||||
"Conversation Timestamp Description": "Will show on the page and HTML files.",
|
||||
"Use 24-hour format": "Use 24-hour format (eg. 23:59)",
|
||||
"Export Metadata": "Export Metadata",
|
||||
"Export Metadata Description": "Add metadata to exported Markdown and HTML files.",
|
||||
"Conversation Delete Alert": "Are you sure you want to delete all selected conversations?",
|
||||
"Conversation Deleted Message": "All selected conversations have been deleted. Please refresh the page to see the changes.",
|
||||
"Please start a conversation first": "Please start a conversation first."
|
||||
}
|
||||
32
packages/userscript/src/locales/jp.json
Normal file
32
packages/userscript/src/locales/jp.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"title": "ChatGPTエクスポーター",
|
||||
"ExportHelper": "エクスポート",
|
||||
"Setting": "設定",
|
||||
"Language": "言語",
|
||||
"Copy Text": "テキストをコピー",
|
||||
"Copied!": "コピーしました!",
|
||||
"Screenshot": "スクリーンショット",
|
||||
"Markdown": "Markdown",
|
||||
"HTML": "HTML",
|
||||
"JSON": "JSON",
|
||||
"Save": "保存",
|
||||
"Delete": "削除",
|
||||
"Select All": "すべて選択",
|
||||
"Export": "エクスポート",
|
||||
"Error": "エラー",
|
||||
"Loading": "読み込み中",
|
||||
"Preview": "プレビュー",
|
||||
"File Name": "ファイル名",
|
||||
"Export All": "すべてエクスポート",
|
||||
"Exporter Settings": "エクスポーター設定",
|
||||
"Export Dialog Title": "会話をエクスポート",
|
||||
"Available variables": "使用可能な変数",
|
||||
"Conversation Timestamp": "会話のタイムスタンプ",
|
||||
"Conversation Timestamp Description": "ページとHTMLファイルに表示されます。",
|
||||
"Use 24-hour format": "24時間形式を使用する (例: 23:59)",
|
||||
"Export Metadata": "メタデータをエクスポート",
|
||||
"Export Metadata Description": "エクスポートされたMarkdownおよびHTMLファイルにメタデータを追加します。",
|
||||
"Conversation Delete Alert": "選択したすべての会話を削除してもよろしいですか?",
|
||||
"Conversation Deleted Message": "選択したすべての会話が削除されました。変更を表示するには、ページを更新してください。",
|
||||
"Please start a conversation first": "まず会話を開始してください。"
|
||||
}
|
||||
32
packages/userscript/src/locales/zh-Hans.json
Normal file
32
packages/userscript/src/locales/zh-Hans.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"title": "ChatGPT Exporter",
|
||||
"ExportHelper": "导出助手",
|
||||
"Setting": "设置",
|
||||
"Language": "语言",
|
||||
"Copy Text": "复制文字",
|
||||
"Copied!": "已复制!",
|
||||
"Screenshot": "截屏",
|
||||
"Markdown": "Markdown",
|
||||
"HTML": "HTML",
|
||||
"JSON": "JSON",
|
||||
"Save": "保存",
|
||||
"Delete": "删除",
|
||||
"Select All": "全选",
|
||||
"Export": "导出",
|
||||
"Error": "错误",
|
||||
"Loading": "加载中",
|
||||
"Preview": "预览",
|
||||
"File Name": "文件名",
|
||||
"Export All": "批量导出",
|
||||
"Exporter Settings": "导出设置",
|
||||
"Export Dialog Title": "导出对话",
|
||||
"Available variables": "可用变量",
|
||||
"Conversation Timestamp": "对话时间戳",
|
||||
"Conversation Timestamp Description": "会添加至页面以及 HTML 导出。",
|
||||
"Use 24-hour format": "使用24小时制 (例如 23:59)",
|
||||
"Export Metadata": "导出元数据",
|
||||
"Export Metadata Description": "会添加至 Markdown 以及 HTML 导出。",
|
||||
"Conversation Delete Alert": "确定要删除所有选取的对话?",
|
||||
"Conversation Deleted Message": "所有所选的对话已删除。请刷新页面。",
|
||||
"Please start a conversation first": "请先开始一次对话。"
|
||||
}
|
||||
32
packages/userscript/src/locales/zh-Hant.json
Normal file
32
packages/userscript/src/locales/zh-Hant.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"title": "ChatGPT Exporter",
|
||||
"ExportHelper": "Export",
|
||||
"Setting": "設定",
|
||||
"Language": "語言",
|
||||
"Copy Text": "複製文字",
|
||||
"Copied!": "已複製!",
|
||||
"Screenshot": "截圖",
|
||||
"Markdown": "Markdown",
|
||||
"HTML": "HTML",
|
||||
"JSON": "JSON",
|
||||
"Save": "保存",
|
||||
"Delete": "刪除",
|
||||
"Select All": "全選",
|
||||
"Export": "匯出",
|
||||
"Error": "錯誤",
|
||||
"Loading": "載入中",
|
||||
"Preview": "預覽",
|
||||
"File Name": "檔案名稱",
|
||||
"Export All": "批量匯出",
|
||||
"Exporter Settings": "設定",
|
||||
"Export Dialog Title": "匯出對話",
|
||||
"Available variables": "可用變數",
|
||||
"Conversation Timestamp": "對話時間戳",
|
||||
"Conversation Timestamp Description": "會添加至頁面以及 HTML 匯出。",
|
||||
"Use 24-hour format": "使用24小時制 (例如 23:59)",
|
||||
"Export Metadata": "匯出元資料",
|
||||
"Export Metadata Description": "會添加至 Markdown 以及 HTML 匯出。",
|
||||
"Conversation Delete Alert": "確定要刪除所有選取的對話?",
|
||||
"Conversation Deleted Message": "所有選取的對話已刪除。請重新整理頁面。",
|
||||
"Please start a conversation first": "請先開始一次對話。"
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { Menu } from './ui/Menu'
|
||||
import { SecondaryToolbar } from './ui/SecondaryToolbar'
|
||||
import { onloadSafe } from './utils/utils'
|
||||
|
||||
import './i18n'
|
||||
import './styles/missing-tailwind.css'
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
padding: 0 15px;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
height: 35px;
|
||||
}
|
||||
.Button.green {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { deleteConversation, fetchAllConversations, fetchConversation } from '../api'
|
||||
import { exportAllToHtml } from '../exporter/html'
|
||||
import { exportAllToJson } from '../exporter/json'
|
||||
@@ -34,11 +35,13 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
|
||||
loading,
|
||||
error,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="SelectToolbar">
|
||||
<CheckBox
|
||||
label="Select All"
|
||||
label={t('Select All')}
|
||||
disabled={disabled}
|
||||
checked={selected.length === conversations.length}
|
||||
onCheckedChange={(checked) => {
|
||||
@@ -47,8 +50,8 @@ const ConversationSelect: FC<ConversationSelectProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<ul className="SelectList">
|
||||
{loading && <li className="SelectItem">Loading...</li>}
|
||||
{error && <li className="SelectItem">Error: {error}</li>}
|
||||
{loading && <li className="SelectItem">{t('Loading')}...</li>}
|
||||
{error && <li className="SelectItem">{t('Error')}: {error}</li>}
|
||||
{conversations.map(c => (
|
||||
<li className="SelectItem" key={c.id}>
|
||||
<CheckBox
|
||||
@@ -76,6 +79,7 @@ interface ExportDialogProps {
|
||||
}
|
||||
|
||||
export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange, children }) => {
|
||||
const { t } = useTranslation()
|
||||
const { enableMeta, exportMetaList } = useSettingContext()
|
||||
const metaList = useMemo(() => enableMeta ? exportMetaList : [], [enableMeta, exportMetaList])
|
||||
|
||||
@@ -129,10 +133,10 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
|
||||
setProcessing(false)
|
||||
setConversations(conversations.filter(c => !selected.some(s => s.id === c.id)))
|
||||
setSelected([])
|
||||
alert('All selected conversations have been deleted. Please refresh the page to see the changes.')
|
||||
alert(t('Conversation Deleted Message'))
|
||||
})
|
||||
return () => off()
|
||||
}, [deleteQueue, conversations, selected])
|
||||
}, [deleteQueue, conversations, selected, t])
|
||||
|
||||
const exportAll = useCallback(() => {
|
||||
if (disabled) return
|
||||
@@ -152,7 +156,7 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
|
||||
const deleteAll = useCallback(() => {
|
||||
if (disabled) return
|
||||
|
||||
const result = confirm('Are you sure you want to delete all selected conversations?')
|
||||
const result = confirm(t('Conversation Delete Alert'))
|
||||
if (!result) return
|
||||
|
||||
deleteQueue.clear()
|
||||
@@ -165,7 +169,7 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
|
||||
})
|
||||
|
||||
deleteQueue.start()
|
||||
}, [disabled, selected, deleteQueue])
|
||||
}, [disabled, selected, deleteQueue, t])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true)
|
||||
@@ -186,7 +190,7 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="DialogOverlay" />
|
||||
<Dialog.Content className="DialogContent">
|
||||
<Dialog.Title className="DialogTitle">Export Conversations</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">{t('Export Dialog Title')}</Dialog.Title>
|
||||
|
||||
<ConversationSelect
|
||||
conversations={conversations}
|
||||
@@ -199,15 +203,15 @@ export const ExportDialog: FC<ExportDialogProps> = ({ format, open, onOpenChange
|
||||
<div className="flex mt-6" style={{ justifyContent: 'space-between' }}>
|
||||
<select className="Select" disabled={processing} value={exportType} onChange={e => setExportType(e.currentTarget.value)}>
|
||||
{exportAllOptions.map(({ label }) => (
|
||||
<option key={label} value={label}>{label}</option>
|
||||
<option key={t(label)} value={label}>{label}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex flex-grow"></div>
|
||||
<button className="Button red" disabled={disabled} onClick={deleteAll}>
|
||||
Delete
|
||||
{t('Delete')}
|
||||
</button>
|
||||
<button className="Button green ml-4" disabled={disabled} onClick={exportAll}>
|
||||
Export
|
||||
{t('Export')}
|
||||
</button>
|
||||
</div>
|
||||
{processing && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as HoverCard from '@radix-ui/react-hover-card'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { exportToHtml } from '../exporter/html'
|
||||
import { exportToPng } from '../exporter/image'
|
||||
import { exportToJson } from '../exporter/json'
|
||||
@@ -21,6 +22,7 @@ But History feature is disabled by OpenAI temporarily.
|
||||
We all have to wait for them to bring it back.`
|
||||
|
||||
function MenuInner({ container }: { container: HTMLDivElement }) {
|
||||
const { t } = useTranslation()
|
||||
const disabled = getHistoryDisabled()
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -85,7 +87,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
|
||||
<HoverCard.Trigger>
|
||||
<MenuItem
|
||||
className="mt-1"
|
||||
text="Export"
|
||||
text={t('ExportHelper')}
|
||||
icon={IconArrowRightFromBracket}
|
||||
onClick={() => {
|
||||
setOpen(true)
|
||||
@@ -116,37 +118,37 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
|
||||
onOpenChange={setSettingOpen}
|
||||
>
|
||||
<div className="row-full">
|
||||
<MenuItem text="Setting" icon={IconSetting} />
|
||||
<MenuItem text={t('Setting')} icon={IconSetting} />
|
||||
</div>
|
||||
</SettingDialog>
|
||||
|
||||
<MenuItem
|
||||
text="Copy Text"
|
||||
successText="Copied!"
|
||||
text={t('Copy Text')}
|
||||
successText={t('Copied!')}
|
||||
icon={() => <IconCopy className="w-4 h-4" />}
|
||||
className="row-full"
|
||||
onClick={onClickText}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Screenshot"
|
||||
text={t('Screenshot')}
|
||||
icon={IconCamera}
|
||||
className="row-half"
|
||||
onClick={onClickPng}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Markdown"
|
||||
text={t('Markdown')}
|
||||
icon={IconMarkdown}
|
||||
className="row-half"
|
||||
onClick={onClickMarkdown}
|
||||
/>
|
||||
<MenuItem
|
||||
text="HTML"
|
||||
text={t('HTML')}
|
||||
icon={FileCode}
|
||||
className="row-half"
|
||||
onClick={onClickHtml}
|
||||
/>
|
||||
<MenuItem
|
||||
text="JSON"
|
||||
text={t('JSON')}
|
||||
icon={IconJSON}
|
||||
className="row-half"
|
||||
onClick={onClickJSON}
|
||||
@@ -158,7 +160,7 @@ function MenuInner({ container }: { container: HTMLDivElement }) {
|
||||
>
|
||||
<div className="row-full">
|
||||
<MenuItem
|
||||
text="Export All"
|
||||
text={t('Export All')}
|
||||
icon={IconZip}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as Dialog from '@radix-ui/react-dialog'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import sanitize from 'sanitize-filename'
|
||||
import { baseUrl } from '../constants'
|
||||
import { useTitle } from '../hooks/useTitle'
|
||||
import { LOCALES } from '../i18n'
|
||||
import { getChatIdFromUrl } from '../page'
|
||||
import { getFileNameWithFormat } from '../utils/download'
|
||||
import { timestamp as _timestamp, dateStr } from '../utils/utils'
|
||||
@@ -31,6 +33,7 @@ export const SettingDialog: FC<SettingDialogProps> = ({
|
||||
enableMeta, setEnableMeta,
|
||||
exportMetaList, setExportMetaList,
|
||||
} = useSettingContext()
|
||||
const { t, i18n } = useTranslation()
|
||||
const _title = useTitle()
|
||||
const date = dateStr()
|
||||
const timestamp = _timestamp()
|
||||
@@ -51,17 +54,35 @@ export const SettingDialog: FC<SettingDialogProps> = ({
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="DialogOverlay" />
|
||||
<Dialog.Content className="DialogContent">
|
||||
<Dialog.Title className="DialogTitle">Exporter Setting</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">{t('Exporter Settings')}</Dialog.Title>
|
||||
|
||||
<dl className="space-y-6">
|
||||
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
|
||||
<div>
|
||||
<dt className="text-md font-medium text-gray-800 dark:text-white">
|
||||
File Name
|
||||
{`${t('Language')} 🌐`}
|
||||
</dt>
|
||||
<dd>
|
||||
<select
|
||||
className="Select mt-3"
|
||||
value={i18n.language}
|
||||
onChange={e => i18n.changeLanguage(e.currentTarget.value)}
|
||||
>
|
||||
{LOCALES.map(({ name, code }) => (
|
||||
<option key={code} value={code}>{name}</option>
|
||||
))}
|
||||
</select>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
|
||||
<div>
|
||||
<dt className="text-md font-medium text-gray-800 dark:text-white">
|
||||
{t('File Name')}
|
||||
</dt>
|
||||
<dd>
|
||||
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Available variables:{' '}
|
||||
{t('Available variables')}:{' '}
|
||||
<Variable name="{title}" title={title} />
|
||||
,{' '}
|
||||
<Variable name="{date}" title={date} />
|
||||
@@ -72,7 +93,7 @@ export const SettingDialog: FC<SettingDialogProps> = ({
|
||||
</p>
|
||||
<input className="Input mt-4" id="filename" value={format} onChange={e => setFormat(e.currentTarget.value)} />
|
||||
<p className="mt-1 text-sm text-gray-700 dark:text-gray-300">
|
||||
Preview:{' '}
|
||||
{t('Preview')}:{' '}
|
||||
<span className="select-all" style={{ 'text-decoration': 'underline', 'text-underline-offset': 4 }}>{preview}</span>
|
||||
</p>
|
||||
</dd>
|
||||
@@ -81,14 +102,14 @@ export const SettingDialog: FC<SettingDialogProps> = ({
|
||||
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
|
||||
<div>
|
||||
<dt className="text-md font-medium text-gray-800 dark:text-white">
|
||||
Conversation Timestamp
|
||||
{t('Conversation Timestamp')}
|
||||
</dt>
|
||||
<dd className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Will show on the page and HTML files.
|
||||
{t('Conversation Timestamp Description')}
|
||||
{enableTimestamp && (
|
||||
<div className="mt-2">
|
||||
<Toggle
|
||||
label="Use 24-hour format (eg. 23:59)"
|
||||
label={t('Use 24-hour format')}
|
||||
checked={timeStamp24H}
|
||||
onCheckedUpdate={setTimeStamp24H}
|
||||
/>
|
||||
@@ -103,15 +124,15 @@ export const SettingDialog: FC<SettingDialogProps> = ({
|
||||
<div className="relative flex bg-white dark:bg-white/5 rounded p-4">
|
||||
<div>
|
||||
<dt className="text-md font-medium text-gray-800 dark:text-white">
|
||||
Export Metadata
|
||||
{t('Export Metadata')}
|
||||
</dt>
|
||||
<dd className="text-sm text-gray-700 dark:text-gray-300">
|
||||
Add metadata to exported Markdown and HTML files.
|
||||
{t('Export Metadata Description')}
|
||||
|
||||
{enableMeta && (
|
||||
<>
|
||||
<p className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
Available variables:{' '}
|
||||
{t('Available variables')}:{' '}
|
||||
<Variable name="{title}" title={title} />
|
||||
,{' '}
|
||||
<Variable name="{date}" title={date} />
|
||||
@@ -174,7 +195,7 @@ export const SettingDialog: FC<SettingDialogProps> = ({
|
||||
</dl>
|
||||
<div className="flex mt-6" style={{ justifyContent: 'flex-end' }}>
|
||||
<Dialog.Close asChild>
|
||||
<button className="Button green">Save</button>
|
||||
<button className="Button green font-bold">{t('Save')}</button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<Dialog.Close asChild>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"src/locales/*.json",
|
||||
"package.json",
|
||||
"vite.config.ts"
|
||||
]
|
||||
|
||||
41
pnpm-lock.yaml
generated
41
pnpm-lock.yaml
generated
@@ -45,6 +45,7 @@ importers:
|
||||
eventemitter3: ^5.0.0
|
||||
hast-util-to-html: ^8.0.4
|
||||
html2canvas: ^1.4.1
|
||||
i18next: ^22.4.14
|
||||
jszip: 3.9.1
|
||||
mdast: ^3.0.0
|
||||
mdast-util-from-markdown: ^1.3.0
|
||||
@@ -54,6 +55,7 @@ importers:
|
||||
mdast-util-to-markdown: ^1.5.0
|
||||
micromark-extension-gfm: ^2.0.1
|
||||
preact: ^10.13.2
|
||||
react-i18next: ^12.2.0
|
||||
sanitize-filename: ^1.6.3
|
||||
sentinel-js: ^0.0.5
|
||||
urlcat: ^2.0.4
|
||||
@@ -67,6 +69,7 @@ importers:
|
||||
eventemitter3: 5.0.0
|
||||
hast-util-to-html: 8.0.4
|
||||
html2canvas: 1.4.1
|
||||
i18next: 22.4.14
|
||||
jszip: 3.9.1
|
||||
mdast: 3.0.0
|
||||
mdast-util-from-markdown: 1.3.0
|
||||
@@ -76,6 +79,7 @@ importers:
|
||||
mdast-util-to-markdown: 1.5.0
|
||||
micromark-extension-gfm: 2.0.1
|
||||
preact: 10.13.2
|
||||
react-i18next: 12.2.0_57ubdvajp6562okxygabugvlve
|
||||
sanitize-filename: 1.6.3
|
||||
sentinel-js: 0.0.5_4xqsksf5w62wic46kncafprj3e
|
||||
urlcat: 2.0.4
|
||||
@@ -3156,6 +3160,12 @@ packages:
|
||||
lru-cache: 6.0.0
|
||||
dev: true
|
||||
|
||||
/html-parse-stringify/3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
dev: false
|
||||
|
||||
/html-void-elements/2.0.1:
|
||||
resolution: {integrity: sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==}
|
||||
dev: false
|
||||
@@ -3202,6 +3212,12 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/i18next/22.4.14:
|
||||
resolution: {integrity: sha512-VtLPtbdwGn0+DAeE00YkiKKXadkwg+rBUV+0v8v0ikEjwdiJ0gmYChVE4GIa9HXymY6wKapkL93vGT7xpq6aTw==}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.0
|
||||
dev: false
|
||||
|
||||
/ignore/5.2.4:
|
||||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -4707,6 +4723,26 @@ packages:
|
||||
scheduler: 0.23.0
|
||||
dev: false
|
||||
|
||||
/react-i18next/12.2.0_57ubdvajp6562okxygabugvlve:
|
||||
resolution: {integrity: sha512-5XeVgSygaGfyFmDd2WcXvINRw2WEC1XviW1LXY/xLOEMzsCFRwKqfnHN+hUjla8ZipbVJR27GCMSuTr0BhBBBQ==}
|
||||
peerDependencies:
|
||||
i18next: '>= 19.0.0'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.0
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 22.4.14
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
dev: false
|
||||
|
||||
/react-is/16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
dev: true
|
||||
@@ -5728,6 +5764,11 @@ packages:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
/void-elements/3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/web-namespaces/2.0.1:
|
||||
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
|
||||
dev: false
|
||||
|
||||
Reference in New Issue
Block a user