diff --git a/.i18nrc.js b/.i18nrc.js index 6f78936852..58c01941df 100644 --- a/.i18nrc.js +++ b/.i18nrc.js @@ -25,7 +25,7 @@ module.exports = defineConfig({ ], temperature: 0, saveImmediately: true, - modelName: 'gpt-5-mini', + modelName: 'gpt-4.1-mini', experimental: { jsonMode: true, }, diff --git a/locales/ar/components.json b/locales/ar/components.json index 17dceee295..a6c77c3b8f 100644 --- a/locales/ar/components.json +++ b/locales/ar/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "عودة" }, + "HtmlPreview": { + "actions": { + "download": "تنزيل", + "preview": "معاينة" + }, + "iframeTitle": "معاينة HTML", + "mode": { + "code": "رمز", + "preview": "معاينة" + }, + "title": "معاينة HTML" + }, "ImageUpload": { "actions": { "changeImage": "انقر لتغيير الصورة", diff --git a/locales/bg-BG/components.json b/locales/bg-BG/components.json index 05abda0544..c70d376288 100644 --- a/locales/bg-BG/components.json +++ b/locales/bg-BG/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Назад" }, + "HtmlPreview": { + "actions": { + "download": "Изтегляне", + "preview": "Преглед" + }, + "iframeTitle": "HTML Преглед", + "mode": { + "code": "Код", + "preview": "Преглед" + }, + "title": "HTML Преглед" + }, "ImageUpload": { "actions": { "changeImage": "Кликнете, за да смените изображението", diff --git a/locales/de-DE/components.json b/locales/de-DE/components.json index 954947c8eb..469d75760d 100644 --- a/locales/de-DE/components.json +++ b/locales/de-DE/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Zurück" }, + "HtmlPreview": { + "actions": { + "download": "Herunterladen", + "preview": "Vorschau" + }, + "iframeTitle": "HTML-Vorschau", + "mode": { + "code": "Code", + "preview": "Vorschau" + }, + "title": "HTML-Vorschau" + }, "ImageUpload": { "actions": { "changeImage": "Klicken, um das Bild zu ändern", diff --git a/locales/en-US/components.json b/locales/en-US/components.json index 39441225e8..e9eae8eace 100644 --- a/locales/en-US/components.json +++ b/locales/en-US/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Back" }, + "HtmlPreview": { + "actions": { + "download": "Download", + "preview": "Preview" + }, + "iframeTitle": "HTML Preview", + "mode": { + "code": "Code", + "preview": "Preview" + }, + "title": "HTML Preview" + }, "ImageUpload": { "actions": { "changeImage": "Click to change image", diff --git a/locales/es-ES/components.json b/locales/es-ES/components.json index 738bf2ce1e..54b7e3185c 100644 --- a/locales/es-ES/components.json +++ b/locales/es-ES/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Regresar" }, + "HtmlPreview": { + "actions": { + "download": "Descargar", + "preview": "Vista previa" + }, + "iframeTitle": "Vista previa HTML", + "mode": { + "code": "Código", + "preview": "Vista previa" + }, + "title": "Vista previa HTML" + }, "ImageUpload": { "actions": { "changeImage": "Haz clic para cambiar la imagen", diff --git a/locales/fa-IR/components.json b/locales/fa-IR/components.json index 6792bd63a7..8c159c9b6b 100644 --- a/locales/fa-IR/components.json +++ b/locales/fa-IR/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "بازگشت" }, + "HtmlPreview": { + "actions": { + "download": "دانلود", + "preview": "پیش‌نمایش" + }, + "iframeTitle": "پیش‌نمایش HTML", + "mode": { + "code": "کد", + "preview": "پیش‌نمایش" + }, + "title": "پیش‌نمایش HTML" + }, "ImageUpload": { "actions": { "changeImage": "برای تغییر تصویر کلیک کنید", diff --git a/locales/fr-FR/components.json b/locales/fr-FR/components.json index b65477d514..255d96d882 100644 --- a/locales/fr-FR/components.json +++ b/locales/fr-FR/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Retour" }, + "HtmlPreview": { + "actions": { + "download": "Télécharger", + "preview": "Aperçu" + }, + "iframeTitle": "Aperçu HTML", + "mode": { + "code": "Code", + "preview": "Aperçu" + }, + "title": "Aperçu HTML" + }, "ImageUpload": { "actions": { "changeImage": "Cliquez pour changer l'image", diff --git a/locales/it-IT/components.json b/locales/it-IT/components.json index d455c37a31..4d4f10d3ea 100644 --- a/locales/it-IT/components.json +++ b/locales/it-IT/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Indietro" }, + "HtmlPreview": { + "actions": { + "download": "Scarica", + "preview": "Anteprima" + }, + "iframeTitle": "Anteprima HTML", + "mode": { + "code": "Codice", + "preview": "Anteprima" + }, + "title": "Anteprima HTML" + }, "ImageUpload": { "actions": { "changeImage": "Clicca per cambiare immagine", diff --git a/locales/ja-JP/components.json b/locales/ja-JP/components.json index e0c2e8f43a..9a45dc39dd 100644 --- a/locales/ja-JP/components.json +++ b/locales/ja-JP/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "戻る" }, + "HtmlPreview": { + "actions": { + "download": "ダウンロード", + "preview": "プレビュー" + }, + "iframeTitle": "HTML プレビュー", + "mode": { + "code": "コード", + "preview": "プレビュー" + }, + "title": "HTML プレビュー" + }, "ImageUpload": { "actions": { "changeImage": "画像を変更するにはクリックしてください", diff --git a/locales/ko-KR/components.json b/locales/ko-KR/components.json index 5361a76213..87591319ec 100644 --- a/locales/ko-KR/components.json +++ b/locales/ko-KR/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "뒤로 가기" }, + "HtmlPreview": { + "actions": { + "download": "다운로드", + "preview": "미리보기" + }, + "iframeTitle": "HTML 미리보기", + "mode": { + "code": "코드", + "preview": "미리보기" + }, + "title": "HTML 미리보기" + }, "ImageUpload": { "actions": { "changeImage": "이미지 변경 클릭", diff --git a/locales/nl-NL/components.json b/locales/nl-NL/components.json index 8c1c472576..94cddba9f1 100644 --- a/locales/nl-NL/components.json +++ b/locales/nl-NL/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Terug" }, + "HtmlPreview": { + "actions": { + "download": "Downloaden", + "preview": "Voorbeeld" + }, + "iframeTitle": "HTML Voorbeeld", + "mode": { + "code": "Code", + "preview": "Voorbeeld" + }, + "title": "HTML Voorbeeld" + }, "ImageUpload": { "actions": { "changeImage": "Klik om afbeelding te wijzigen", diff --git a/locales/pl-PL/components.json b/locales/pl-PL/components.json index 0cdcc89fba..b1c5d59688 100644 --- a/locales/pl-PL/components.json +++ b/locales/pl-PL/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Wróć" }, + "HtmlPreview": { + "actions": { + "download": "Pobierz", + "preview": "Podgląd" + }, + "iframeTitle": "Podgląd HTML", + "mode": { + "code": "Kod", + "preview": "Podgląd" + }, + "title": "Podgląd HTML" + }, "ImageUpload": { "actions": { "changeImage": "Kliknij, aby zmienić obraz", diff --git a/locales/pt-BR/components.json b/locales/pt-BR/components.json index 39313982e0..1603e5c26f 100644 --- a/locales/pt-BR/components.json +++ b/locales/pt-BR/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Voltar" }, + "HtmlPreview": { + "actions": { + "download": "Baixar", + "preview": "Visualizar" + }, + "iframeTitle": "Visualização HTML", + "mode": { + "code": "Código", + "preview": "Visualizar" + }, + "title": "Visualização HTML" + }, "ImageUpload": { "actions": { "changeImage": "Clique para alterar a imagem", diff --git a/locales/ru-RU/components.json b/locales/ru-RU/components.json index c7069169b3..6e316217a7 100644 --- a/locales/ru-RU/components.json +++ b/locales/ru-RU/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Назад" }, + "HtmlPreview": { + "actions": { + "download": "Скачать", + "preview": "Предпросмотр" + }, + "iframeTitle": "Предпросмотр HTML", + "mode": { + "code": "Код", + "preview": "Предпросмотр" + }, + "title": "Предпросмотр HTML" + }, "ImageUpload": { "actions": { "changeImage": "Нажмите, чтобы изменить изображение", diff --git a/locales/tr-TR/components.json b/locales/tr-TR/components.json index 43f35c2bd6..9cdacaf1a5 100644 --- a/locales/tr-TR/components.json +++ b/locales/tr-TR/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Geri dön" }, + "HtmlPreview": { + "actions": { + "download": "İndir", + "preview": "Önizleme" + }, + "iframeTitle": "HTML Önizlemesi", + "mode": { + "code": "Kod", + "preview": "Önizleme" + }, + "title": "HTML Önizlemesi" + }, "ImageUpload": { "actions": { "changeImage": "Resmi değiştirmek için tıklayın", diff --git a/locales/vi-VN/components.json b/locales/vi-VN/components.json index 6999445d59..ddd19dd6d2 100644 --- a/locales/vi-VN/components.json +++ b/locales/vi-VN/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "Quay lại" }, + "HtmlPreview": { + "actions": { + "download": "Tải xuống", + "preview": "Xem trước" + }, + "iframeTitle": "Xem trước HTML", + "mode": { + "code": "Mã", + "preview": "Xem trước" + }, + "title": "Xem trước HTML" + }, "ImageUpload": { "actions": { "changeImage": "Nhấn để thay đổi hình ảnh", diff --git a/locales/zh-CN/components.json b/locales/zh-CN/components.json index df7bd58601..5a61f7390e 100644 --- a/locales/zh-CN/components.json +++ b/locales/zh-CN/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "返回" }, + "HtmlPreview": { + "actions": { + "download": "下载", + "preview": "预览" + }, + "iframeTitle": "HTML 预览", + "mode": { + "code": "代码", + "preview": "预览" + }, + "title": "HTML 预览" + }, "ImageUpload": { "actions": { "changeImage": "点击更换图片", diff --git a/locales/zh-TW/components.json b/locales/zh-TW/components.json index efae8223a9..ff0a4a9b86 100644 --- a/locales/zh-TW/components.json +++ b/locales/zh-TW/components.json @@ -73,6 +73,18 @@ "GoBack": { "back": "返回" }, + "HtmlPreview": { + "actions": { + "download": "下載", + "preview": "預覽" + }, + "iframeTitle": "HTML 預覽", + "mode": { + "code": "程式碼", + "preview": "預覽" + }, + "title": "HTML 預覽" + }, "ImageUpload": { "actions": { "changeImage": "點擊更換圖片", diff --git a/packages/const/src/index.ts b/packages/const/src/index.ts index 916b48205b..aadd8fe257 100644 --- a/packages/const/src/index.ts +++ b/packages/const/src/index.ts @@ -1,4 +1,6 @@ +export * from './branding'; export * from './image'; +export * from './layoutTokens'; export * from './message'; export * from './settings'; export * from './version'; diff --git a/packages/utils/src/client/index.ts b/packages/utils/src/client/index.ts new file mode 100644 index 0000000000..5abf30cd88 --- /dev/null +++ b/packages/utils/src/client/index.ts @@ -0,0 +1,2 @@ +export * from './downloadFile'; +export * from './exportFile'; diff --git a/src/components/HtmlPreview/HtmlPreviewAction.tsx b/src/components/HtmlPreview/HtmlPreviewAction.tsx new file mode 100644 index 0000000000..201ef7da7a --- /dev/null +++ b/src/components/HtmlPreview/HtmlPreviewAction.tsx @@ -0,0 +1,32 @@ +import { ActionIcon } from '@lobehub/ui'; +import { Eye } from 'lucide-react'; +import { memo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import HtmlPreviewDrawer from './PreviewDrawer'; + +interface HtmlPreviewActionProps { + content: string; + size?: number; +} + +const HtmlPreviewAction = memo(({ content, size }) => { + const { t } = useTranslation('components'); + const [open, setOpen] = useState(false); + + return ( + <> + setOpen(true)} + size={size} + title={t('HtmlPreview.actions.preview')} + /> + setOpen(false)} open={open} /> + + ); +}); + +HtmlPreviewAction.displayName = 'HtmlPreviewAction'; + +export default HtmlPreviewAction; diff --git a/src/components/HtmlPreview/PreviewDrawer.tsx b/src/components/HtmlPreview/PreviewDrawer.tsx new file mode 100644 index 0000000000..f51f4dcbbc --- /dev/null +++ b/src/components/HtmlPreview/PreviewDrawer.tsx @@ -0,0 +1,133 @@ +import { exportFile } from '@lobechat/utils/client'; +import { Block, Button, Highlighter, Segmented } from '@lobehub/ui'; +import { Drawer } from 'antd'; +import { createStyles } from 'antd-style'; +import { Code2, Download, Eye } from 'lucide-react'; +import { memo, useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import { isDesktop } from '@/const/version'; +import { TITLE_BAR_HEIGHT } from '@/features/ElectronTitlebar'; + +const useStyles = createStyles(({ css }) => ({ + container: css` + height: 100%; + `, + iframe: css` + width: 100%; + height: 100%; + border: none; + `, +})); + +interface HtmlPreviewDrawerProps { + content: string; + onClose: () => void; + open: boolean; +} + +const HtmlPreviewDrawer = memo(({ content, open, onClose }) => { + const { styles } = useStyles(); + const { t } = useTranslation('components'); + const [mode, setMode] = useState<'preview' | 'code'>('preview'); + + const htmlContent = content; + + const extractTitle = useCallback(() => { + const m = htmlContent.match(/([\S\s]*?)<\/title>/i); + return m ? m[1].trim() : undefined; + }, [htmlContent]); + + const sanitizeFileName = useCallback((name: string) => { + return name + .replaceAll(/["*/:<>?\\|]/g, '-') + .replaceAll(/\s+/g, ' ') + .trim() + .slice(0, 100); + }, []); + + const onDownload = useCallback(() => { + const title = extractTitle(); + const base = title ? sanitizeFileName(title) : `chat-html-preview-${Date.now()}`; + exportFile(content, `${base}.html`); + }, [content, extractTitle, sanitizeFileName]); + + const Title = ( + <Flexbox align={'center'} horizontal justify={'space-between'} style={{ width: '100%' }}> + {t('HtmlPreview.title')} + <Segmented + onChange={(v) => setMode(v as 'preview' | 'code')} + options={[ + { + label: ( + <Flexbox align={'center'} gap={6} horizontal> + <Eye size={16} /> + {t('HtmlPreview.mode.preview')} + </Flexbox> + ), + value: 'preview', + }, + { + label: ( + <Flexbox align={'center'} gap={6} horizontal> + <Code2 size={16} /> + {t('HtmlPreview.mode.code')} + </Flexbox> + ), + value: 'code', + }, + ]} + value={mode} + /> + <Button + color={'default'} + icon={<Download size={16} />} + onClick={onDownload} + variant={'filled'} + > + {t('HtmlPreview.actions.download')} + </Button> + </Flexbox> + ); + + return ( + <Drawer + destroyOnHidden + height={isDesktop ? `calc(100vh - ${TITLE_BAR_HEIGHT}px)` : '100vh'} + onClose={onClose} + open={open} + placement="bottom" + styles={{ + body: { height: '100%', padding: 0 }, + header: { paddingBlock: 8, paddingInline: 12 }, + }} + title={Title} + > + {mode === 'preview' ? ( + <Block className={styles.container}> + <iframe + className={styles.iframe} + sandbox="allow-scripts allow-same-origin" + srcDoc={content} + title={t('HtmlPreview.iframeTitle')} + /> + </Block> + ) : ( + <Block className={styles.container}> + <Highlighter + language={'html'} + showLanguage={false} + style={{ height: '100%', overflow: 'auto' }} + > + {htmlContent} + </Highlighter> + </Block> + )} + </Drawer> + ); +}); + +HtmlPreviewDrawer.displayName = 'HtmlPreviewDrawer'; + +export default HtmlPreviewDrawer; diff --git a/src/components/HtmlPreview/index.ts b/src/components/HtmlPreview/index.ts new file mode 100644 index 0000000000..12af3b2780 --- /dev/null +++ b/src/components/HtmlPreview/index.ts @@ -0,0 +1,2 @@ +export { default as HtmlPreviewAction } from './HtmlPreviewAction'; +export { default as HtmlPreviewDrawer } from './PreviewDrawer'; diff --git a/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/index.tsx b/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/index.tsx index eee6aa364e..8210362f17 100644 --- a/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/index.tsx +++ b/src/features/Conversation/components/ChatItem/ShareMessageModal/ShareText/index.tsx @@ -1,3 +1,4 @@ +import { exportFile } from '@lobechat/utils/client'; import { Button, copyToClipboard } from '@lobehub/ui'; import { App } from 'antd'; import isEqual from 'fast-deep-equal'; @@ -10,7 +11,6 @@ import { useIsMobile } from '@/hooks/useIsMobile'; import { useChatStore } from '@/store/chat'; import { topicSelectors } from '@/store/chat/selectors'; import { ChatMessage } from '@/types/message'; -import { exportFile } from '@/utils/client/exportFile'; import { useStyles } from '../style'; import Preview from './Preview'; diff --git a/src/features/Conversation/components/ChatItem/index.tsx b/src/features/Conversation/components/ChatItem/index.tsx index 1be3f5473a..ab0fcc26c3 100644 --- a/src/features/Conversation/components/ChatItem/index.tsx +++ b/src/features/Conversation/components/ChatItem/index.tsx @@ -6,6 +6,7 @@ import { MouseEventHandler, ReactNode, memo, use, useCallback, useMemo } from 'r import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; +import { HtmlPreviewAction } from '@/components/HtmlPreview'; import { isDesktop } from '@/const/version'; import ChatItem from '@/features/ChatItem'; import { VirtuosoContext } from '@/features/Conversation/components/VirtualizedList/VirtuosoContext'; @@ -33,6 +34,14 @@ import { normalizeThinkTags, processWithArtifact } from './utils'; const rehypePlugins = markdownElements.map((element) => element.rehypePlugin).filter(Boolean); const remarkPlugins = markdownElements.map((element) => element.remarkPlugin).filter(Boolean); +const isHtmlCode = (content: string, language: string) => { + return ( + language === 'html' || + (language === '' && content.includes('<html>')) || + (language === '' && content.includes('<!DOCTYPE html>')) + ); +}; + const useStyles = createStyles(({ css, prefixCls }) => ({ loading: css` opacity: 0.6; @@ -175,6 +184,20 @@ const Item = memo<ChatListItemProps>( () => ({ animated, citations: item?.role === 'user' ? undefined : item?.search?.citations, + componentProps: { + highlight: { + actionsRender: ({ content, actionIconSize, language, originalNode }: any) => { + const showHtmlPreview = isHtmlCode(content, language); + + return ( + <> + {showHtmlPreview && <HtmlPreviewAction content={content} size={actionIconSize} />} + {originalNode} + </> + ); + }, + }, + }, components, customRender: markdownCustomRender, enableCustomFootnotes: item?.role === 'assistant', diff --git a/src/features/ShareModal/ShareJSON/index.tsx b/src/features/ShareModal/ShareJSON/index.tsx index 091d96df7a..fa3bca3cd5 100644 --- a/src/features/ShareModal/ShareJSON/index.tsx +++ b/src/features/ShareModal/ShareJSON/index.tsx @@ -1,3 +1,5 @@ +import { FORM_STYLE } from '@lobechat/const'; +import { exportFile } from '@lobechat/utils/client'; import { Button, Form, type FormItemProps, copyToClipboard } from '@lobehub/ui'; import { App, Switch } from 'antd'; import isEqual from 'fast-deep-equal'; @@ -6,13 +8,11 @@ import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; -import { FORM_STYLE } from '@/const/layoutTokens'; import { useIsMobile } from '@/hooks/useIsMobile'; import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/selectors'; import { useChatStore } from '@/store/chat'; import { chatSelectors, topicSelectors } from '@/store/chat/selectors'; -import { exportFile } from '@/utils/client/exportFile'; import { useStyles } from '../style'; import Preview from './Preview'; diff --git a/src/features/ShareModal/ShareText/index.tsx b/src/features/ShareModal/ShareText/index.tsx index 62d549509a..53ad090986 100644 --- a/src/features/ShareModal/ShareText/index.tsx +++ b/src/features/ShareModal/ShareText/index.tsx @@ -1,3 +1,4 @@ +import { exportFile } from '@lobechat/utils/client'; import { Button, Form, type FormItemProps, copyToClipboard } from '@lobehub/ui'; import { App, Switch } from 'antd'; import isEqual from 'fast-deep-equal'; @@ -12,7 +13,6 @@ import { useAgentStore } from '@/store/agent'; import { agentSelectors } from '@/store/agent/selectors'; import { useChatStore } from '@/store/chat'; import { chatSelectors, topicSelectors } from '@/store/chat/selectors'; -import { exportFile } from '@/utils/client/exportFile'; import { useStyles } from '../style'; import Preview from './Preview'; diff --git a/src/locales/default/components.ts b/src/locales/default/components.ts index 38a6fd60d1..a9d8cabb09 100644 --- a/src/locales/default/components.ts +++ b/src/locales/default/components.ts @@ -75,6 +75,18 @@ export default { GoBack: { back: '返回', }, + HtmlPreview: { + actions: { + download: '下载', + preview: '预览', + }, + iframeTitle: 'HTML 预览', + mode: { + code: '代码', + preview: '预览', + }, + title: 'HTML 预览', + }, ImageUpload: { actions: { changeImage: '点击更换图片', diff --git a/src/services/config.ts b/src/services/config.ts index d46c870781..1f18789bfc 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -1,11 +1,9 @@ +import { BRANDING_NAME, isDeprecatedEdition, isServerMode } from '@lobechat/const'; +import { downloadFile, exportJSONFile } from '@lobechat/utils/client'; import dayjs from 'dayjs'; -import { BRANDING_NAME } from '@/const/branding'; -import { isDeprecatedEdition, isServerMode } from '@/const/version'; import { CURRENT_CONFIG_VERSION } from '@/migrations'; import { ImportPgDataStructure } from '@/types/export'; -import { downloadFile } from '@/utils/client/downloadFile'; -import { exportJSONFile } from '@/utils/client/exportFile'; import { exportService } from './export'; import { configService as deprecatedExportService } from './export/_deprecated';