diff --git a/docs/README.zh.md b/docs/README.zh.md index 670a8420f2..1e4876a820 100644 --- a/docs/README.zh.md +++ b/docs/README.zh.md @@ -155,4 +155,4 @@ yinsenho@cherry-ai.com # ⭐️ Star 记录 -[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline) +[![Star History Chart](https://api.star-history.com/svg?repos=kangfenmao/cherry-studio&type=Timeline)](https://star-history.com/#kangfenmao/cherry-studio&Timeline) \ No newline at end of file diff --git a/src/renderer/src/components/Scrollbar/index.tsx b/src/renderer/src/components/Scrollbar/index.tsx index 670e37895a..857a8404e2 100644 --- a/src/renderer/src/components/Scrollbar/index.tsx +++ b/src/renderer/src/components/Scrollbar/index.tsx @@ -2,12 +2,13 @@ import { throttle } from 'lodash' import { FC, useCallback, useEffect, useRef, useState } from 'react' import styled from 'styled-components' -interface Props extends React.HTMLAttributes { +interface Props extends Omit, 'onScroll'> { right?: boolean - ref?: any + ref?: React.RefObject + onScroll?: () => void // Custom onScroll prop for useScrollPosition's handleScroll } -const Scrollbar: FC = ({ ref, ...props }: Props & { ref?: React.RefObject }) => { +const Scrollbar: FC = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => { const [isScrolling, setIsScrolling] = useState(false) const timeoutRef = useRef(null) @@ -21,18 +22,31 @@ const Scrollbar: FC = ({ ref, ...props }: Props & { ref?: React.RefObject timeoutRef.current = setTimeout(() => setIsScrolling(false), 1500) }, []) - const throttledHandleScroll = throttle(handleScroll, 200) + const throttledInternalScrollHandler = throttle(handleScroll, 200) + + // Combined scroll handler + const combinedOnScroll = useCallback(() => { + // Event is available if needed by internal handler + throttledInternalScrollHandler() // Call internal logic + if (externalOnScroll) { + externalOnScroll() // Call external logic (from useScrollPosition) + } + }, [throttledInternalScrollHandler, externalOnScroll]) useEffect(() => { return () => { timeoutRef.current && clearTimeout(timeoutRef.current) - throttledHandleScroll.cancel() + throttledInternalScrollHandler.cancel() } - }, [throttledHandleScroll]) + }, [throttledInternalScrollHandler]) return ( - - {props.children} + + {children} ) } diff --git a/src/renderer/src/hooks/useScrollPosition.ts b/src/renderer/src/hooks/useScrollPosition.ts index ddcd54028f..b3f4b5d512 100644 --- a/src/renderer/src/hooks/useScrollPosition.ts +++ b/src/renderer/src/hooks/useScrollPosition.ts @@ -7,7 +7,9 @@ export default function useScrollPosition(key: string) { const handleScroll = throttle(() => { const position = containerRef.current?.scrollTop ?? 0 - window.keyv.set(scrollKey, position) + window.requestAnimationFrame(() => { + window.keyv.set(scrollKey, position) + }) }, 100) useEffect(() => { diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index bf997b1213..a6f77f1596 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -598,6 +598,7 @@ "delete.confirm.content": "Are you sure you want to delete the selected {{count}} message(s)?", "delete.failed": "Delete Failed", "delete.success": "Delete Successful", + "empty_url": "Failed to download image, possibly due to prompt containing sensitive content or prohibited words", "error.chunk_overlap_too_large": "Chunk overlap cannot be greater than chunk size", "error.dimension_too_large": "Content size is too large", "error.enter.api.host": "Please enter your API host first", @@ -809,6 +810,7 @@ "model": "Model Version", "aspect_ratio": "Aspect Ratio", "style_type": "Style", + "rendering_speed": "Rendering Speed", "learn_more": "Learn More", "prompt_placeholder_edit": "Enter your image description, text drawing uses \"double quotes\" to wrap", "proxy_required": "Currently, you need to open a proxy to view the generated images, it will be supported in the future", @@ -816,6 +818,20 @@ "image_file_retry": "Please re-upload an image first", "image_placeholder": "No image available", "image_retry": "Retry", + "translating": "Translating...", + "style_types": { + "auto": "Auto", + "general": "General", + "realistic": "Realistic", + "design": "Design", + "3d": "3D", + "anime": "Anime" + }, + "rendering_speeds": { + "default": "Default", + "turbo": "Turbo", + "quality": "Quality" + }, "mode": { "generate": "Draw", "edit": "Edit", @@ -823,20 +839,22 @@ "upscale": "Upscale" }, "generate": { - "model_tip": "Model version: V2 is the latest model of the interface, V2A is the fast model, V_1 is the first-generation model, _TURBO is the acceleration version", + "model_tip": "Model version: V3 is the latest version, V2 is the previous model, V2A is the fast model, V_1 is the first-generation model, _TURBO is the acceleration version", "number_images_tip": "Number of images to generate", "seed_tip": "Controls image generation randomness for reproducible results", "negative_prompt_tip": "Describe unwanted elements, only for V_1, V_1_TURBO, V_2, and V_2_TURBO", "magic_prompt_option_tip": "Intelligently enhances prompts for better results", - "style_type_tip": "Image generation style for V_2 and above" + "style_type_tip": "Image generation style for V_2 and above", + "rendering_speed_tip": "Controls rendering speed vs. quality trade-off, only available for V_3" }, "edit": { "image_file": "Edited Image", - "model_tip": "Only supports V_2 and V_2_TURBO versions", + "model_tip": "V3 and V2 versions supported", "number_images_tip": "Number of edited results to generate", "style_type_tip": "Style for edited image, only for V_2 and above", "seed_tip": "Controls editing randomness", - "magic_prompt_option_tip": "Intelligently enhances editing prompts" + "magic_prompt_option_tip": "Intelligently enhances editing prompts", + "rendering_speed_tip": "Controls rendering speed vs. quality trade-off, only available for V_3" }, "remix": { "model_tip": "Select AI model version for remixing", @@ -847,7 +865,8 @@ "seed_tip": "Control the randomness of the mixed result", "style_type_tip": "Style for remixed image, only for V_2 and above", "negative_prompt_tip": "Describe unwanted elements in remix results", - "magic_prompt_option_tip": "Intelligently enhances remix prompts" + "magic_prompt_option_tip": "Intelligently enhances remix prompts", + "rendering_speed_tip": "Controls rendering speed vs. quality trade-off, only available for V_3" }, "upscale": { "image_file": "Image to upscale", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index a778b823bb..a36c08f1a2 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -599,6 +599,8 @@ "delete.failed": "削除に失敗しました", "delete.success": "削除が成功しました", "error.chunk_overlap_too_large": "チャンクの重なりは、チャンクサイズを超えることはできません", + "empty_url": "画像をダウンロードできません。プロンプトに不適切なコンテンツや禁止用語が含まれている可能性があります", + "error.chunk_overlap_too_large": "チャンクのオーバーラップがチャンクサイズより大きくなることはできません", "error.dimension_too_large": "内容のサイズが大きすぎます", "error.enter.api.host": "APIホストを入力してください", "error.enter.api.key": "APIキーを入力してください", @@ -816,6 +818,19 @@ "image_file_retry": "画像を先にアップロードしてください", "image_placeholder": "画像がありません", "image_retry": "再試行", + "style_types": { + "auto": "自動", + "general": "一般", + "realistic": "リアル", + "design": "デザイン", + "3d": "3D", + "anime": "アニメ" + }, + "rendering_speeds": { + "default": "デフォルト", + "turbo": "高速", + "quality": "高品質" + }, "mode": { "generate": "画像生成", "edit": "部分編集", @@ -828,7 +843,8 @@ "seed_tip": "画像生成のランダム性を制御して、同じ生成結果を再現します", "negative_prompt_tip": "画像に含めたくない内容を説明します", "magic_prompt_option_tip": "生成効果を向上させるための提示詞を最適化します", - "style_type_tip": "画像生成スタイル、V_2 以上のバージョンでのみ適用" + "style_type_tip": "画像生成スタイル、V_2 以上のバージョンでのみ適用", + "rendering_speed_tip": "レンダリング速度と品質のバランスを調整します。V_3バージョンでのみ利用可能です" }, "edit": { "image_file": "編集画像", @@ -836,7 +852,8 @@ "number_images_tip": "生成される編集結果の数", "style_type_tip": "編集後の画像スタイル、V_2 以上のバージョンでのみ適用", "seed_tip": "編集結果のランダム性を制御します", - "magic_prompt_option_tip": "編集効果を向上させるための提示詞を最適化します" + "magic_prompt_option_tip": "編集効果を向上させるための提示詞を最適化します", + "rendering_speed_tip": "レンダリング速度と品質のバランスを調整します。V_3バージョンでのみ利用可能です" }, "remix": { "model_tip": "リミックスに使用する AI モデルのバージョンを選択します", @@ -847,7 +864,8 @@ "seed_tip": "リミックス結果のランダム性を制御します", "style_type_tip": "リミックス後の画像スタイル、V_2 以上のバージョンでのみ適用", "negative_prompt_tip": "リミックス結果に含めたくない内容を説明します", - "magic_prompt_option_tip": "リミックス効果を向上させるための提示詞を最適化します" + "magic_prompt_option_tip": "リミックス効果を向上させるための提示詞を最適化します", + "rendering_speed_tip": "レンダリング速度と品質のバランスを調整します。V_3バージョンでのみ利用可能です" }, "upscale": { "image_file": "拡大する画像", @@ -858,7 +876,9 @@ "number_images_tip": "生成される拡大結果の数", "seed_tip": "拡大結果のランダム性を制御します", "magic_prompt_option_tip": "拡大効果を向上させるための提示詞を最適化します" - } + }, + "rendering_speed": "レンダリング速度", + "translating": "翻訳中..." }, "prompts": { "explanation": "この概念を説明してください", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index 627c3fbfc6..658e999ce6 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -599,6 +599,8 @@ "delete.failed": "Ошибка удаления", "delete.success": "Удаление успешно", "error.chunk_overlap_too_large": "Перекрытие фрагментов не может быть больше размера фрагмента.", + "empty_url": "Не удалось загрузить изображение, возможно, запрос содержит конфиденциальный контент или запрещенные слова", + "error.chunk_overlap_too_large": "Перекрытие фрагментов не может быть больше размера фрагмента", "error.dimension_too_large": "Размер содержимого слишком велик", "error.enter.api.host": "Пожалуйста, введите ваш API хост", "error.enter.api.key": "Пожалуйста, введите ваш API ключ", @@ -815,7 +817,21 @@ "image_file_required": "Пожалуйста, сначала загрузите изображение", "image_file_retry": "Пожалуйста, сначала загрузите изображение", "image_placeholder": "Изображение недоступно", - "image_retry": "Попробовать снова", + "image_retry": "Повторить", + "translating": "Перевод...", + "style_types": { + "auto": "Авто", + "general": "Общий", + "realistic": "Реалистичный", + "design": "Дизайн", + "3d": "3D", + "anime": "Аниме" + }, + "rendering_speeds": { + "default": "По умолчанию", + "turbo": "Быстро", + "quality": "Качественно" + }, "mode": { "generate": "Рисование", "edit": "Редактирование", @@ -823,31 +839,34 @@ "upscale": "Увеличение" }, "generate": { - "model_tip": "Версия модели: V2 — последняя модель интерфейса, V2A — быстрая модель, V_1 — первая модель, _TURBO — ускоренная версия", - "number_images_tip": "Количество изображений для генерации", - "seed_tip": "Контролирует случайный характер генерации изображений для воспроизводимых результатов", - "negative_prompt_tip": "Опишите элементы, которые вы не хотите включать в изображение, поддерживаются только версии V_1, V_1_TURBO, V_2 и V_2_TURBO", - "magic_prompt_option_tip": "Улучшает генерацию изображений с помощью интеллектуального оптимизирования промптов", - "style_type_tip": "Стиль генерации изображений, поддерживается только для версий V_2 и выше" + "model_tip": "Версия модели: V2 - новейшая API модель, V2A - быстрая модель, V_1 - первое поколение, _TURBO - ускоренная версия", + "number_images_tip": "Количество изображений для одновременной генерации", + "seed_tip": "Контролирует случайность генерации изображений для воспроизведения одинаковых результатов", + "negative_prompt_tip": "Описывает, что вы не хотите видеть в изображении", + "magic_prompt_option_tip": "Интеллектуально оптимизирует подсказки для улучшения эффекта генерации", + "style_type_tip": "Стиль генерации изображений, доступен только для версий V_2 и выше", + "rendering_speed_tip": "Управляет балансом между скоростью рендеринга и качеством, доступно только для V_3" }, "edit": { - "image_file": "Редактируемое изображение", + "image_file": "Изображение для редактирования", "model_tip": "Частичное редактирование поддерживается только версиями V_2 и V_2_TURBO", - "number_images_tip": "Количество редактированных результатов для генерации", - "style_type_tip": "Стиль редактированного изображения, поддерживается только для версий V_2 и выше", - "seed_tip": "Контролирует случайный характер редактирования изображений для воспроизводимых результатов", - "magic_prompt_option_tip": "Улучшает редактирование изображений с помощью интеллектуального оптимизирования промптов" + "number_images_tip": "Количество результатов редактирования для генерации", + "style_type_tip": "Стиль изображения после редактирования, доступен только для версий V_2 и выше", + "seed_tip": "Контролирует случайность результатов редактирования", + "magic_prompt_option_tip": "Интеллектуально оптимизирует подсказки для улучшения эффекта редактирования", + "rendering_speed_tip": "Управляет балансом между скоростью рендеринга и качеством, доступно только для V_3" }, "remix": { - "model_tip": "Выберите версию AI-модели для перемешивания", - "image_file": "Ссылка на изображение", - "image_weight": "Вес изображения", - "image_weight_tip": "Насколько сильно влияние изображения на результат", - "number_images_tip": "Количество перемешанных результатов для генерации", - "seed_tip": "Контролирует случайный характер перемешивания изображений для воспроизводимых результатов", - "style_type_tip": "Стиль перемешанного изображения, поддерживается только для версий V_2 и выше", - "negative_prompt_tip": "Опишите элементы, которые вы не хотите включать в изображение", - "magic_prompt_option_tip": "Улучшает перемешивание изображений с помощью интеллектуального оптимизирования промптов" + "model_tip": "Выберите версию AI модели для ремикса", + "image_file": "Референсное изображение", + "image_weight": "Вес референсного изображения", + "image_weight_tip": "Регулирует степень влияния референсного изображения", + "number_images_tip": "Количество результатов ремикса для генерации", + "seed_tip": "Контролирует случайность результатов ремикса", + "style_type_tip": "Стиль изображения после ремикса, доступен только для версий V_2 и выше", + "negative_prompt_tip": "Описывает, что вы не хотите видеть в результатах ремикса", + "magic_prompt_option_tip": "Интеллектуально оптимизирует подсказки для улучшения эффекта ремикса", + "rendering_speed_tip": "Управляет балансом между скоростью рендеринга и качеством, доступно только для V_3" }, "upscale": { "image_file": "Изображение для увеличения", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index d0b6d7b919..97bc885cfa 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -598,6 +598,7 @@ "delete.confirm.content": "确认删除选中的{{count}}条消息吗?", "delete.failed": "删除失败", "delete.success": "删除成功", + "empty_url": "无法下载图片,可能是提示词包含敏感内容或违禁词汇", "error.chunk_overlap_too_large": "分段重叠不能大于分段大小", "error.dimension_too_large": "内容尺寸过大", "error.enter.api.host": "请输入您的 API 地址", @@ -809,6 +810,7 @@ "model": "版本", "aspect_ratio": "画幅比例", "style_type": "风格", + "rendering_speed": "渲染速度", "learn_more": "了解更多", "prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹", "proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连", @@ -816,6 +818,20 @@ "image_file_retry": "请重新上传图片", "image_placeholder": "暂无图片", "image_retry": "重试", + "translating": "翻译中...", + "style_types": { + "auto": "自动", + "general": "通用", + "realistic": "写实", + "design": "设计", + "3d": "3D", + "anime": "动漫" + }, + "rendering_speeds": { + "default": "默认", + "turbo": "快速", + "quality": "高质量" + }, "mode": { "generate": "绘图", "edit": "编辑", @@ -823,20 +839,22 @@ "upscale": "放大" }, "generate": { - "model_tip": "模型版本:V2 为接口最新模型,V2A 为快速模型、V_1 为初代模型,_TURBO 为加速版本", + "model_tip": "模型版本:V3 为最新版本,V2 为之前版本,V2A 为快速模型、V_1 为初代模型,_TURBO 为加速版本", "number_images_tip": "单次出图数量", "seed_tip": "控制图像生成的随机性,用于复现相同的生成结果", "negative_prompt_tip": "描述不想在图像中出现的元素,仅支持 V_1、V_1_TURBO、V_2 和 V_2_TURBO 版本", "magic_prompt_option_tip": "智能优化提示词以提升生成效果", - "style_type_tip": "图像生成风格,仅适用于 V_2 及以上版本" + "style_type_tip": "图像生成风格,仅适用于 V_2 及以上版本", + "rendering_speed_tip": "控制渲染速度与质量的平衡,仅适用于 V_3 版本" }, "edit": { "image_file": "编辑的图像", - "model_tip": "局部编辑仅支持 V_2 和 V_2_TURBO 版本", + "model_tip": "支持 V3 和 V2 版本", "number_images_tip": "生成的编辑结果数量", "style_type_tip": "编辑后的图像风格,仅适用于 V_2 及以上版本", "seed_tip": "控制编辑结果的随机性", - "magic_prompt_option_tip": "智能优化编辑提示词" + "magic_prompt_option_tip": "智能优化编辑提示词", + "rendering_speed_tip": "控制渲染速度与质量的平衡,仅适用于 V_3 版本" }, "remix": { "model_tip": "选择重混使用的 AI 模型版本", @@ -847,7 +865,8 @@ "seed_tip": "控制重混结果的随机性", "style_type_tip": "重混后的图像风格,仅适用于 V_2 及以上版本", "negative_prompt_tip": "描述不想在重混结果中出现的元素", - "magic_prompt_option_tip": "智能优化重混提示词" + "magic_prompt_option_tip": "智能优化重混提示词", + "rendering_speed_tip": "控制渲染速度与质量之间的平衡,仅适用于V_3版本" }, "upscale": { "image_file": "需要放大的图片", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index fd61cf46f9..31e6b52013 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -598,6 +598,8 @@ "delete.confirm.content": "確認刪除選中的 {{count}} 條訊息嗎?", "delete.failed": "刪除失敗", "delete.success": "刪除成功", + "copy.success": "複製成功", + "empty_url": "無法下載圖片,可能是提示詞包含敏感內容或違禁詞彙", "error.chunk_overlap_too_large": "分段重疊不能大於分段大小", "error.dimension_too_large": "內容尺寸過大", "error.enter.api.host": "請先輸入您的 API 主機地址", @@ -816,6 +818,20 @@ "image_file_retry": "請重新上傳圖片", "image_placeholder": "無圖片", "image_retry": "重試", + "translating": "翻譯中...", + "style_types": { + "auto": "自動", + "general": "通用", + "realistic": "寫實", + "design": "設計", + "3d": "3D", + "anime": "動漫" + }, + "rendering_speeds": { + "default": "預設", + "turbo": "快速", + "quality": "高品質" + }, "mode": { "generate": "繪圖", "edit": "編輯", @@ -823,20 +839,22 @@ "upscale": "放大" }, "generate": { - "model_tip": "模型版本:V2 為接口最新模型,V2A 為快速模型、V_1 為初代模型,_TURBO 為加速版本", - "number_images_tip": "單次出圖數量", - "seed_tip": "控制圖像生成的隨機性,用於重現相同的生成結果", - "negative_prompt_tip": "描述不想在圖像中出現的元素,僅支援 V_1、V_1_TURBO、V_2 和 V_2_TURBO 版本", - "magic_prompt_option_tip": "智能優化提示詞以提升生成效果", - "style_type_tip": "圖像生成風格,僅適用於 V_2 及以上版本" + "model_tip": "模型版本:V2 是最新 API 模型,V2A 是高速模型,V_1 是初代模型,_TURBO 是高速處理版", + "number_images_tip": "一次生成的圖片數量", + "seed_tip": "控制圖像生成的隨機性,以重現相同的生成結果", + "negative_prompt_tip": "描述不想在圖像中出現的內容", + "magic_prompt_option_tip": "智能優化生成效果的提示詞", + "style_type_tip": "圖像生成風格,僅適用於 V_2 及以上版本", + "rendering_speed_tip": "控制渲染速度與品質之間的平衡,僅適用於V_3版本" }, "edit": { - "image_file": "編輯的圖像", - "model_tip": "局部編輯僅支援 V_2 和 V_2_TURBO 版本", + "image_file": "編輯圖像", + "model_tip": "部分編輯僅支持 V_2 和 V_2_TURBO 版本", "number_images_tip": "生成的編輯結果數量", "style_type_tip": "編輯後的圖像風格,僅適用於 V_2 及以上版本", "seed_tip": "控制編輯結果的隨機性", - "magic_prompt_option_tip": "智能優化編輯提示詞" + "magic_prompt_option_tip": "智能優化編輯提示詞", + "rendering_speed_tip": "控制渲染速度與品質之間的平衡,僅適用於V_3版本" }, "remix": { "model_tip": "選擇重混使用的 AI 模型版本", @@ -847,7 +865,8 @@ "seed_tip": "控制重混結果的隨機性", "style_type_tip": "重混後的圖像風格,僅適用於 V_2 及以上版本", "negative_prompt_tip": "描述不想在重混結果中出現的元素", - "magic_prompt_option_tip": "智能優化重混提示詞" + "magic_prompt_option_tip": "智能優化重混提示詞", + "rendering_speed_tip": "控制渲染速度與品質之間的平衡,僅適用於V_3版本" }, "upscale": { "image_file": "需要放大的圖片", @@ -858,7 +877,8 @@ "number_images_tip": "生成的放大結果數量", "seed_tip": "控制放大結果的隨機性", "magic_prompt_option_tip": "智能優化放大提示詞" - } + }, + "rendering_speed": "渲染速度" }, "prompts": { "explanation": "幫我解釋一下這個概念", diff --git a/src/renderer/src/pages/home/Messages/Messages.tsx b/src/renderer/src/pages/home/Messages/Messages.tsx index 4349dbdf5e..7b9c447055 100644 --- a/src/renderer/src/pages/home/Messages/Messages.tsx +++ b/src/renderer/src/pages/home/Messages/Messages.tsx @@ -3,6 +3,7 @@ import Scrollbar from '@renderer/components/Scrollbar' import { LOAD_MORE_COUNT } from '@renderer/config/constant' import { useAssistant } from '@renderer/hooks/useAssistant' import { useMessageOperations, useTopicMessages } from '@renderer/hooks/useMessageOperations' +import useScrollPosition from '@renderer/hooks/useScrollPosition' import { useSettings } from '@renderer/hooks/useSettings' import { useShortcut } from '@renderer/hooks/useShortcuts' import { autoRenameTopic, getTopic } from '@renderer/hooks/useTopic' @@ -26,7 +27,7 @@ import { updateCodeBlock } from '@renderer/utils/markdown' import { getMainTextContent } from '@renderer/utils/messageUtils/find' import { isTextLikeBlock } from '@renderer/utils/messageUtils/is' import { last } from 'lodash' -import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import InfiniteScroll from 'react-infinite-scroll-component' import styled from 'styled-components' @@ -46,13 +47,15 @@ interface MessagesProps { onFirstUpdate?(): void } -const Messages: FC = ({ assistant, topic, setActiveTopic, onComponentUpdate, onFirstUpdate }) => { +const Messages: React.FC = ({ assistant, topic, setActiveTopic, onComponentUpdate, onFirstUpdate }) => { + const { containerRef: scrollContainerRef, handleScroll: handleScrollPosition } = useScrollPosition( + `topic-${topic.id}` + ) const { t } = useTranslation() const { showPrompt, showTopics, topicPosition, showAssistants, messageNavigation } = useSettings() const { isMultiSelectMode, selectedMessageIds, handleSelectMessage } = useChatContext() const { updateTopic, addTopic } = useAssistant(assistant.id) const dispatch = useAppDispatch() - const containerRef = useRef(null) const [displayMessages, setDisplayMessages] = useState([]) const [hasMore, setHasMore] = useState(false) const [isLoadingMore, setIsLoadingMore] = useState(false) @@ -158,16 +161,16 @@ const Messages: FC = ({ assistant, topic, setActiveTopic, onCompo }, [showAssistants, showTopics, topicPosition]) const scrollToBottom = useCallback(() => { - if (containerRef.current) { + if (scrollContainerRef.current) { requestAnimationFrame(() => { - if (containerRef.current) { - containerRef.current.scrollTo({ - top: containerRef.current.scrollHeight + if (scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + top: scrollContainerRef.current.scrollHeight }) } }) } - }, []) + }, [scrollContainerRef]) const clearTopic = useCallback( async (data: Topic) => { @@ -201,14 +204,14 @@ const Messages: FC = ({ assistant, topic, setActiveTopic, onCompo }) }), EventEmitter.on(EVENT_NAMES.COPY_TOPIC_IMAGE, async () => { - await captureScrollableDivAsBlob(containerRef, async (blob) => { + await captureScrollableDivAsBlob(scrollContainerRef, async (blob) => { if (blob) { await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) } }) }), EventEmitter.on(EVENT_NAMES.EXPORT_TOPIC_IMAGE, async () => { - const imageData = await captureScrollableDivAsDataURL(containerRef) + const imageData = await captureScrollableDivAsDataURL(scrollContainerRef) if (imageData) { window.api.file.saveImage(removeSpecialCharactersForFileName(topic.name), imageData) } @@ -347,6 +350,7 @@ const Messages: FC = ({ assistant, topic, setActiveTopic, onCompo paddingTop: showPrompt ? 10 : 0 }} key={assistant.id} + onScroll={handleScrollPosition} $right={topicPosition === 'left'}> = ({ Options }) => { // 不使用 AiProvider 的通用规则,而是直接调用自定义接口 try { if (mode === 'generate') { - const requestData = { - image_request: { - prompt, - model: painting.model, - aspect_ratio: painting.aspectRatio, - num_images: painting.numImages, - style_type: painting.styleType, - seed: painting.seed ? +painting.seed : undefined, - negative_prompt: painting.negativePrompt || undefined, - magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' + if (painting.model === 'V_3') { + // V3 API uses different endpoint and parameters format + const formData = new FormData() + formData.append('prompt', prompt) + + // 确保渲染速度参数正确传递 + const renderSpeed = painting.renderingSpeed || 'DEFAULT' + console.log('使用渲染速度:', renderSpeed) + formData.append('rendering_speed', renderSpeed) + + formData.append('num_images', String(painting.numImages || 1)) + + // Convert aspect ratio format from ASPECT_1_1 to 1x1 for V3 API + if (painting.aspectRatio) { + const aspectRatioValue = painting.aspectRatio.replace('ASPECT_', '').replace('_', 'x').toLowerCase() + console.log('转换后的宽高比:', aspectRatioValue) + formData.append('aspect_ratio', aspectRatioValue) } + + if (painting.styleType && painting.styleType !== 'AUTO') { + // 确保样式类型与API文档一致,保持大写形式 + // V3 API支持的样式类型: AUTO, GENERAL, REALISTIC, DESIGN + const styleType = painting.styleType + console.log('使用样式类型:', styleType) + formData.append('style_type', styleType) + } else { + // 确保明确设置默认样式类型 + console.log('使用默认样式类型: AUTO') + formData.append('style_type', 'AUTO') + } + + if (painting.seed) { + console.log('使用随机种子:', painting.seed) + formData.append('seed', painting.seed) + } + + if (painting.negativePrompt) { + console.log('使用负面提示词:', painting.negativePrompt) + formData.append('negative_prompt', painting.negativePrompt) + } + + if (painting.magicPromptOption !== undefined) { + const magicPrompt = painting.magicPromptOption ? 'ON' : 'OFF' + console.log('使用魔法提示词:', magicPrompt) + formData.append('magic_prompt', magicPrompt) + } + + // 打印所有FormData内容 + console.log('FormData内容:') + for (const pair of formData.entries()) { + console.log(pair[0] + ': ' + pair[1]) + } + + body = formData + // For V3 endpoints - 使用模板字符串而不是字符串连接 + console.log('API 端点:', `${aihubmixProvider.apiHost}/ideogram/v1/ideogram-v3/generate`) + + // 调整请求头,可能需要指定multipart/form-data + // 注意:FormData会自动设置Content-Type,不应手动设置 + const apiHeaders = { 'Api-Key': aihubmixProvider.apiKey } + + try { + const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/v1/ideogram-v3/generate`, { + method: 'POST', + headers: apiHeaders, + body + }) + + if (!response.ok) { + const errorData = await response.json() + console.error('V3 API错误:', errorData) + throw new Error(errorData.error?.message || '生成图像失败') + } + + const data = await response.json() + console.log('V3 API响应:', data) + const urls = data.data.map((item) => item.url) + + // Rest of the code for handling image downloads is the same + if (urls.length > 0) { + const downloadedFiles = await Promise.all( + urls.map(async (url) => { + try { + // 检查URL是否为空 + if (!url || url.trim() === '') { + console.error('图像URL为空,可能是提示词违禁') + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + return null + } + return await window.api.file.download(url) + } catch (error) { + console.error('下载图像失败:', error) + // 检查是否是URL解析错误 + if ( + error instanceof Error && + (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) + ) { + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + } + return null + } + }) + ) + + const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) + await FileManager.addFiles(validFiles) + updatePaintingState({ files: validFiles, urls }) + } + return + } catch (error: unknown) { + if (error instanceof Error && error.name !== 'AbortError') { + window.modal.error({ + content: getErrorMessage(error), + centered: true + }) + } + } finally { + setIsLoading(false) + dispatch(setGenerating(false)) + setAbortController(null) + } + } else { + // Existing V1/V2 API + const requestData = { + image_request: { + prompt, + model: painting.model, + aspect_ratio: painting.aspectRatio, + num_images: painting.numImages, + style_type: painting.styleType, + seed: painting.seed ? +painting.seed : undefined, + negative_prompt: painting.negativePrompt || undefined, + magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' + } + } + body = JSON.stringify(requestData) + headers['Content-Type'] = 'application/json' } - body = JSON.stringify(requestData) - headers['Content-Type'] = 'application/json' - } else { + } else if (mode === 'remix') { if (!painting.imageFile) { window.modal.error({ content: t('paintings.image_file_required'), @@ -165,67 +295,311 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { }) return } - const form = new FormData() - let imageRequest: Record = { - prompt, - num_images: painting.numImages, - seed: painting.seed ? +painting.seed : undefined, - magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' - } - if (mode === 'remix') { - imageRequest = { - ...imageRequest, + + if (painting.model === 'V_3') { + // V3 Remix API + const formData = new FormData() + formData.append('prompt', prompt) + formData.append('rendering_speed', painting.renderingSpeed || 'DEFAULT') + formData.append('num_images', String(painting.numImages || 1)) + + // Convert aspect ratio format for V3 API + if (painting.aspectRatio) { + const aspectRatioValue = painting.aspectRatio.replace('ASPECT_', '').replace('_', 'x').toLowerCase() + formData.append('aspect_ratio', aspectRatioValue) + } + + if (painting.styleType) { + formData.append('style_type', painting.styleType) + } + + if (painting.seed) { + formData.append('seed', painting.seed) + } + + if (painting.negativePrompt) { + formData.append('negative_prompt', painting.negativePrompt) + } + + if (painting.magicPromptOption !== undefined) { + formData.append('magic_prompt', painting.magicPromptOption ? 'ON' : 'OFF') + } + + if (painting.imageWeight) { + formData.append('image_weight', String(painting.imageWeight)) + } + + // Add the image file + formData.append('image', fileMap[painting.imageFile] as unknown as Blob) + + body = formData + // For V3 Remix endpoint + const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/v1/ideogram-v3/remix`, { + method: 'POST', + headers: { 'Api-Key': aihubmixProvider.apiKey }, + body + }) + + if (!response.ok) { + const errorData = await response.json() + console.error('V3 Remix API错误:', errorData) + throw new Error(errorData.error?.message || '图像混合失败') + } + + const data = await response.json() + console.log('V3 Remix API响应:', data) + const urls = data.data.map((item) => item.url) + + // Handle the downloaded images + if (urls.length > 0) { + const downloadedFiles = await Promise.all( + urls.map(async (url) => { + try { + // 检查URL是否为空 + if (!url || url.trim() === '') { + console.error('图像URL为空,可能是提示词违禁') + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + return null + } + return await window.api.file.download(url) + } catch (error) { + console.error('下载图像失败:', error) + // 检查是否是URL解析错误 + if ( + error instanceof Error && + (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) + ) { + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + } + return null + } + }) + ) + + const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) + await FileManager.addFiles(validFiles) + updatePaintingState({ files: validFiles, urls }) + } + return + } else { + // Existing V1/V2 API for remix + const form = new FormData() + const imageRequest: Record = { + prompt, model: painting.model, aspect_ratio: painting.aspectRatio, image_weight: painting.imageWeight, - style_type: painting.styleType + style_type: painting.styleType, + num_images: painting.numImages, + seed: painting.seed ? +painting.seed : undefined, + negative_prompt: painting.negativePrompt || undefined, + magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' } - } else if (mode === 'upscale') { - imageRequest = { - ...imageRequest, - resemblance: painting.resemblance, - detail: painting.detail + form.append('image_request', JSON.stringify(imageRequest)) + form.append('image_file', fileMap[painting.imageFile] as unknown as Blob) + body = form + } + } else if (mode === 'edit') { + if (!painting.imageFile) { + window.modal.error({ + content: t('paintings.image_file_required'), + centered: true + }) + return + } + if (!fileMap[painting.imageFile]) { + window.modal.error({ + content: t('paintings.image_file_retry'), + centered: true + }) + return + } + + if (painting.model === 'V_3') { + // V3 Edit API + const formData = new FormData() + formData.append('prompt', prompt) + formData.append('rendering_speed', painting.renderingSpeed || 'DEFAULT') + formData.append('num_images', String(painting.numImages || 1)) + + if (painting.styleType) { + formData.append('style_type', painting.styleType) } - } else if (mode === 'edit') { - imageRequest = { - ...imageRequest, + + if (painting.seed) { + formData.append('seed', painting.seed) + } + + if (painting.magicPromptOption !== undefined) { + formData.append('magic_prompt', painting.magicPromptOption ? 'ON' : 'OFF') + } + + // Add the image file + formData.append('image', fileMap[painting.imageFile] as unknown as Blob) + + // Add the mask if available + if (painting.mask) { + formData.append('mask', painting.mask as unknown as Blob) + } + + body = formData + // For V3 Edit endpoint + const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/v1/ideogram-v3/edit`, { + method: 'POST', + headers: { 'Api-Key': aihubmixProvider.apiKey }, + body + }) + + if (!response.ok) { + const errorData = await response.json() + console.error('V3 Edit API错误:', errorData) + throw new Error(errorData.error?.message || '图像编辑失败') + } + + const data = await response.json() + console.log('V3 Edit API响应:', data) + const urls = data.data.map((item) => item.url) + + // Handle the downloaded images + if (urls.length > 0) { + const downloadedFiles = await Promise.all( + urls.map(async (url) => { + try { + // 检查URL是否为空 + if (!url || url.trim() === '') { + console.error('图像URL为空,可能是提示词违禁') + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + return null + } + return await window.api.file.download(url) + } catch (error) { + console.error('下载图像失败:', error) + // 检查是否是URL解析错误 + if ( + error instanceof Error && + (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) + ) { + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + } + return null + } + }) + ) + + const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) + await FileManager.addFiles(validFiles) + updatePaintingState({ files: validFiles, urls }) + } + return + } else { + // Existing V1/V2 API for edit + const form = new FormData() + const imageRequest: Record = { + prompt, model: painting.model, - style_type: painting.styleType + style_type: painting.styleType, + num_images: painting.numImages, + seed: painting.seed ? +painting.seed : undefined, + magic_prompt_option: painting.magicPromptOption ? 'ON' : 'OFF' } + form.append('image_request', JSON.stringify(imageRequest)) + form.append('image_file', fileMap[painting.imageFile] as unknown as Blob) + body = form + } + } else if (mode === 'upscale') { + if (!painting.imageFile) { + window.modal.error({ + content: t('paintings.image_file_required'), + centered: true + }) + return + } + if (!fileMap[painting.imageFile]) { + window.modal.error({ + content: t('paintings.image_file_retry'), + centered: true + }) + return + } + + const form = new FormData() + const imageRequest: Record = { + prompt, + resemblance: painting.resemblance, + detail: painting.detail, + num_images: painting.numImages, + seed: painting.seed ? +painting.seed : undefined, + magic_prompt_option: painting.magicPromptOption ? 'AUTO' : 'OFF' } form.append('image_request', JSON.stringify(imageRequest)) form.append('image_file', fileMap[painting.imageFile] as unknown as Blob) body = form } - // 直接调用自定义接口 - const response = await fetch(aihubmixProvider.apiHost + `/ideogram/` + mode, { method: 'POST', headers, body }) + // 只针对非V3模型使用通用接口 + if (!painting.model?.includes('V_3')) { + // 直接调用自定义接口 + const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/${mode}`, { method: 'POST', headers, body }) - if (!response.ok) { - const errorData = await response.json() - throw new Error(errorData.error?.message || '生成图像失败') - } + if (!response.ok) { + const errorData = await response.json() + console.error('通用API错误:', errorData) + throw new Error(errorData.error?.message || '生成图像失败') + } - const data = await response.json() - const urls = data.data.map((item: any) => item.url) + const data = await response.json() + console.log('通用API响应:', data) + const urls = data.data.map((item) => item.url) - if (urls.length > 0) { - const downloadedFiles = await Promise.all( - urls.map(async (url) => { - try { - return await window.api.file.download(url) - } catch (error) { - console.error('下载图像失败:', error) - return null - } - }) - ) + if (urls.length > 0) { + const downloadedFiles = await Promise.all( + urls.map(async (url) => { + try { + // 检查URL是否为空 + if (!url || url.trim() === '') { + console.error('图像URL为空,可能是提示词违禁') + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + return null + } + return await window.api.file.download(url) + } catch (error) { + console.error('下载图像失败:', error) + // 检查是否是URL解析错误 + if ( + error instanceof Error && + (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) + ) { + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + } + return null + } + }) + ) - const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) + const validFiles = downloadedFiles.filter((file): file is FileType => file !== null) - await FileManager.addFiles(validFiles) + await FileManager.addFiles(validFiles) - updatePaintingState({ files: validFiles, urls }) + updatePaintingState({ files: validFiles, urls }) + } } } catch (error: unknown) { if (error instanceof Error && error.name !== 'AbortError') { @@ -246,9 +620,28 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { const downloadedFiles = await Promise.all( painting.urls.map(async (url) => { try { + // 检查URL是否为空 + if (!url || url.trim() === '') { + console.error('图像URL为空,可能是提示词违禁') + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + return null + } return await window.api.file.download(url) } catch (error) { console.error('下载图像失败:', error) + // 检查是否是URL解析错误 + if ( + error instanceof Error && + (error.message.includes('Failed to parse URL') || error.message.includes('Invalid URL')) + ) { + window.message.warning({ + content: t('message.empty_url'), + key: 'empty-url-warning' + }) + } setIsLoading(false) return null } @@ -363,7 +756,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { // 渲染配置项的函数 const renderConfigItem = (item: ConfigItem, index: number) => { switch (item.type) { - case 'title': + case 'title': { return ( {t(item.title!)} @@ -374,30 +767,60 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => { )} ) - case 'select': + } + case 'select': { + // 处理函数类型的disabled属性 + const isDisabled = typeof item.disabled === 'function' ? item.disabled(item, painting) : item.disabled + + // 处理函数类型的options属性 + const selectOptions = + typeof item.options === 'function' + ? item.options(item, painting).map((option) => ({ + ...option, + label: option.label.startsWith('paintings.') ? t(option.label) : option.label + })) + : item.options?.map((option) => ({ + ...option, + label: option.label.startsWith('paintings.') ? t(option.label) : option.label + })) + return (