Merge branch 'main' into translate-auto-detect
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
diff --git a/dist/utils/tiktoken.js b/dist/utils/tiktoken.js
|
||||
index 8e41ee6f00f2f9c7fa2c59fa2b2f4297634b97aa..6708abfaabee108b8967c3fbd7fccc8dfb95e693 100644
|
||||
--- a/dist/utils/tiktoken.js
|
||||
+++ b/dist/utils/tiktoken.js
|
||||
@@ -3,18 +3,20 @@ import { AsyncCaller } from "./async_caller.js";
|
||||
const cache = {};
|
||||
const caller = /* #__PURE__ */ new AsyncCaller({});
|
||||
export async function getEncoding(encoding) {
|
||||
- if (!(encoding in cache)) {
|
||||
- cache[encoding] = caller
|
||||
- .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
|
||||
- .then((res) => res.json())
|
||||
- .then((data) => new Tiktoken(data))
|
||||
- .catch((e) => {
|
||||
- delete cache[encoding];
|
||||
- throw e;
|
||||
- });
|
||||
- }
|
||||
- return await cache[encoding];
|
||||
+ // if (!(encoding in cache)) {
|
||||
+ // cache[encoding] = caller
|
||||
+ // .fetch(`https://tiktoken.pages.dev/js/${encoding}.json`)
|
||||
+ // .then((res) => res.json())
|
||||
+ // .then((data) => new Tiktoken(data))
|
||||
+ // .catch((e) => {
|
||||
+ // delete cache[encoding];
|
||||
+ // throw e;
|
||||
+ // });
|
||||
+ // }
|
||||
+ // return await cache[encoding];
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
export async function encodingForModel(model) {
|
||||
- return getEncoding(getEncodingNameForModel(model));
|
||||
+ // return getEncoding(getEncodingNameForModel(model));
|
||||
+ throw new Error("TikToken Not implemented");
|
||||
}
|
||||
@@ -36,8 +36,11 @@ files:
|
||||
- '!**/*.{spec,test}.{js,jsx,ts,tsx}'
|
||||
- '!**/*.min.*.map'
|
||||
- '!**/*.d.ts'
|
||||
- '!**/dist/es6/**'
|
||||
- '!**/dist/demo/**'
|
||||
- '!**/amd/**'
|
||||
- '!**/{.DS_Store,Thumbs.db,thumbs.db,__pycache__}'
|
||||
- '!**/{LICENSE,LICENSE.txt,LICENSE-MIT.txt,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}'
|
||||
- '!**/{LICENSE,license,LICENSE.*,*.LICENSE.txt,NOTICE.txt,README.md,readme.md,CHANGELOG.md}'
|
||||
- '!node_modules/rollup-plugin-visualizer'
|
||||
- '!node_modules/js-tiktoken'
|
||||
- '!node_modules/@tavily/core/node_modules/js-tiktoken'
|
||||
|
||||
+1
-19
@@ -9,25 +9,7 @@ const visualizerPlugin = (type: 'renderer' | 'main') => {
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [
|
||||
externalizeDepsPlugin({
|
||||
exclude: [
|
||||
'@cherrystudio/embedjs',
|
||||
'@cherrystudio/embedjs-openai',
|
||||
'@cherrystudio/embedjs-loader-web',
|
||||
'@cherrystudio/embedjs-loader-markdown',
|
||||
'@cherrystudio/embedjs-loader-msoffice',
|
||||
'@cherrystudio/embedjs-loader-xml',
|
||||
'@cherrystudio/embedjs-loader-pdf',
|
||||
'@cherrystudio/embedjs-loader-sitemap',
|
||||
'@cherrystudio/embedjs-libsql',
|
||||
'@cherrystudio/embedjs-loader-image',
|
||||
'p-queue',
|
||||
'webdav'
|
||||
]
|
||||
}),
|
||||
...visualizerPlugin('main')
|
||||
],
|
||||
plugins: [externalizeDepsPlugin(), ...visualizerPlugin('main')],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@main': resolve('src/main'),
|
||||
|
||||
+3
-6
@@ -38,7 +38,6 @@
|
||||
"publish": "yarn build:check && yarn release patch push",
|
||||
"pulish:artifacts": "cd packages/artifacts && npm publish && cd -",
|
||||
"generate:agents": "yarn workspace @cherry-studio/database agents",
|
||||
"generate:icons": "electron-icon-builder --input=./build/logo.png --output=build",
|
||||
"analyze:renderer": "VISUALIZER_RENDERER=true yarn build",
|
||||
"analyze:main": "VISUALIZER_MAIN=true yarn build",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
@@ -112,7 +111,7 @@
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@eslint-react/eslint-plugin": "^1.36.1",
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@google/genai": "^0.13.0",
|
||||
"@google/genai": "^1.0.1",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.4",
|
||||
@@ -138,7 +137,6 @@
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/react-window": "^1",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/ws": "^8",
|
||||
"@uiw/codemirror-extensions-langs": "^4.23.12",
|
||||
"@uiw/codemirror-themes-all": "^4.23.12",
|
||||
"@uiw/react-codemirror": "^4.23.12",
|
||||
@@ -159,7 +157,6 @@
|
||||
"electron": "35.4.0",
|
||||
"electron-builder": "26.0.15",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"electron-vite": "^3.1.0",
|
||||
"emittery": "^1.0.3",
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
@@ -220,13 +217,13 @@
|
||||
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
||||
"@langchain/openai@npm:^0.3.16": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"node-gyp": "^9.1.0",
|
||||
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
||||
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
|
||||
"openai@npm:^4.87.3": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch"
|
||||
"app-builder-lib@npm:26.0.15": "patch:app-builder-lib@npm%3A26.0.15#~/.yarn/patches/app-builder-lib-npm-26.0.15-360e5b0476.patch",
|
||||
"@langchain/core@npm:^0.3.26": "patch:@langchain/core@npm%3A0.3.44#~/.yarn/patches/@langchain-core-npm-0.3.44-41d5c3cb0a.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -111,6 +111,7 @@ export enum IpcChannel {
|
||||
File_WriteWithId = 'file:writeWithId',
|
||||
File_SaveImage = 'file:saveImage',
|
||||
File_Base64Image = 'file:base64Image',
|
||||
File_SaveBase64Image = 'file:saveBase64Image',
|
||||
File_Download = 'file:download',
|
||||
File_Copy = 'file:copy',
|
||||
File_BinaryImage = 'file:binaryImage',
|
||||
|
||||
+1
-8
@@ -18,7 +18,6 @@ import CopilotService from './services/CopilotService'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileService from './services/FileService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import mcpService from './services/MCPService'
|
||||
import NotificationService from './services/NotificationService'
|
||||
@@ -250,6 +249,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId)
|
||||
ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage)
|
||||
ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image)
|
||||
ipcMain.handle(IpcChannel.File_SaveBase64Image, fileManager.saveBase64Image)
|
||||
ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File)
|
||||
ipcMain.handle(IpcChannel.File_Download, fileManager.downloadFile)
|
||||
ipcMain.handle(IpcChannel.File_Copy, fileManager.copyFile)
|
||||
@@ -298,13 +298,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
}
|
||||
})
|
||||
|
||||
// gemini
|
||||
ipcMain.handle(IpcChannel.Gemini_UploadFile, GeminiService.uploadFile)
|
||||
ipcMain.handle(IpcChannel.Gemini_Base64File, GeminiService.base64File)
|
||||
ipcMain.handle(IpcChannel.Gemini_RetrieveFile, GeminiService.retrieveFile)
|
||||
ipcMain.handle(IpcChannel.Gemini_ListFiles, GeminiService.listFiles)
|
||||
ipcMain.handle(IpcChannel.Gemini_DeleteFile, GeminiService.deleteFile)
|
||||
|
||||
// mini window
|
||||
ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
|
||||
ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())
|
||||
|
||||
@@ -268,6 +268,51 @@ class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public saveBase64Image = async (_: Electron.IpcMainInvokeEvent, base64Data: string): Promise<FileType> => {
|
||||
try {
|
||||
if (!base64Data) {
|
||||
throw new Error('Base64 data is required')
|
||||
}
|
||||
|
||||
// 移除 base64 头部信息(如果存在)
|
||||
const base64String = base64Data.replace(/^data:.*;base64,/, '')
|
||||
const buffer = Buffer.from(base64String, 'base64')
|
||||
const uuid = uuidv4()
|
||||
const ext = '.png'
|
||||
const destPath = path.join(this.storageDir, uuid + ext)
|
||||
|
||||
logger.info('[FileStorage] Saving base64 image:', {
|
||||
storageDir: this.storageDir,
|
||||
destPath,
|
||||
bufferSize: buffer.length
|
||||
})
|
||||
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(this.storageDir)) {
|
||||
fs.mkdirSync(this.storageDir, { recursive: true })
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(destPath, buffer)
|
||||
|
||||
const fileMetadata: FileType = {
|
||||
id: uuid,
|
||||
origin_name: uuid + ext,
|
||||
name: uuid + ext,
|
||||
path: destPath,
|
||||
created_at: new Date().toISOString(),
|
||||
size: buffer.length,
|
||||
ext: ext.slice(1),
|
||||
type: getFileType(ext),
|
||||
count: 1
|
||||
}
|
||||
|
||||
return fileMetadata
|
||||
} catch (error) {
|
||||
logger.error('[FileStorage] Failed to save base64 image:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public base64File = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: string; mime: string }> => {
|
||||
const filePath = path.join(this.storageDir, id)
|
||||
const buffer = await fs.promises.readFile(filePath)
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import { File, FileState, GoogleGenAI, Pager } from '@google/genai'
|
||||
import { FileType } from '@types'
|
||||
import fs from 'fs'
|
||||
|
||||
import { CacheService } from './CacheService'
|
||||
|
||||
export class GeminiService {
|
||||
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
|
||||
private static readonly CACHE_DURATION = 3000
|
||||
|
||||
static async uploadFile(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
file: FileType,
|
||||
{ apiKey, baseURL }: { apiKey: string; baseURL: string }
|
||||
): Promise<File> {
|
||||
const sdk = new GoogleGenAI({
|
||||
vertexai: false,
|
||||
apiKey,
|
||||
httpOptions: {
|
||||
baseUrl: baseURL
|
||||
}
|
||||
})
|
||||
|
||||
return await sdk.files.upload({
|
||||
file: file.path,
|
||||
config: {
|
||||
mimeType: 'application/pdf',
|
||||
name: file.id,
|
||||
displayName: file.origin_name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) {
|
||||
return {
|
||||
data: Buffer.from(fs.readFileSync(file.path)).toString('base64'),
|
||||
mimeType: 'application/pdf'
|
||||
}
|
||||
}
|
||||
|
||||
static async retrieveFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string): Promise<File | undefined> {
|
||||
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
|
||||
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
|
||||
if (cachedResponse) {
|
||||
return GeminiService.processResponse(cachedResponse, file)
|
||||
}
|
||||
|
||||
const response = await sdk.files.list()
|
||||
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
|
||||
|
||||
return GeminiService.processResponse(response, file)
|
||||
}
|
||||
|
||||
private static async processResponse(response: Pager<File>, file: FileType) {
|
||||
for await (const f of response) {
|
||||
if (f.state === FileState.ACTIVE) {
|
||||
if (f.displayName === file.origin_name && Number(f.sizeBytes) === file.size) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string): Promise<File[]> {
|
||||
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
|
||||
const files: File[] = []
|
||||
for await (const f of await sdk.files.list()) {
|
||||
files.push(f)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
static async deleteFile(_: Electron.IpcMainInvokeEvent, fileId: string, apiKey: string) {
|
||||
const sdk = new GoogleGenAI({ vertexai: false, apiKey })
|
||||
await sdk.files.delete({ name: fileId })
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,7 @@ const api = {
|
||||
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
|
||||
base64Image: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Base64Image, fileId),
|
||||
saveBase64Image: (data: string) => ipcRenderer.invoke(IpcChannel.File_SaveBase64Image, data),
|
||||
download: (url: string, isUseContentType?: boolean) =>
|
||||
ipcRenderer.invoke(IpcChannel.File_Download, url, isUseContentType),
|
||||
copy: (fileId: string, destPath: string) => ipcRenderer.invoke(IpcChannel.File_Copy, fileId, destPath),
|
||||
|
||||
@@ -5,12 +5,12 @@ export const TOOL_SPECS: Record<string, CodeToolSpec> = {
|
||||
copy: {
|
||||
id: 'copy',
|
||||
type: 'core',
|
||||
order: 10
|
||||
order: 11
|
||||
},
|
||||
download: {
|
||||
id: 'download',
|
||||
type: 'core',
|
||||
order: 11
|
||||
order: 10
|
||||
},
|
||||
edit: {
|
||||
id: 'edit',
|
||||
|
||||
@@ -137,7 +137,7 @@ const MainMenus: FC = () => {
|
||||
const { hideMinappPopup } = useMinappPopup()
|
||||
const { t } = useTranslation()
|
||||
const { pathname } = useLocation()
|
||||
const { sidebarIcons } = useSettings()
|
||||
const { sidebarIcons, defaultPaintingProvider } = useSettings()
|
||||
const { minappShow } = useRuntime()
|
||||
const navigate = useNavigate()
|
||||
const { theme } = useTheme()
|
||||
@@ -158,7 +158,7 @@ const MainMenus: FC = () => {
|
||||
const pathMap = {
|
||||
assistants: '/',
|
||||
agents: '/agents',
|
||||
paintings: '/paintings',
|
||||
paintings: `/paintings/${defaultPaintingProvider}`,
|
||||
translate: '/translate',
|
||||
minapp: '/apps',
|
||||
knowledge: '/knowledge',
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
setAutoCheckUpdate as _setAutoCheckUpdate,
|
||||
setLaunchOnBoot,
|
||||
setLaunchToTray,
|
||||
setPinTopicsToTop,
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
setSidebarIcons,
|
||||
setTargetLanguage,
|
||||
setTheme,
|
||||
SettingsState,
|
||||
setTopicPosition,
|
||||
setPinTopicsToTop,
|
||||
setTray as _setTray,
|
||||
setTrayOnClose,
|
||||
setWindowStyle
|
||||
|
||||
@@ -820,12 +820,12 @@
|
||||
"seed_desc_tip": "The same seed and prompt can generate similar images, setting -1 will generate different results each time",
|
||||
"title": "Images",
|
||||
"magic_prompt_option": "Magic Prompt",
|
||||
"model": "Model Version",
|
||||
"model": "Model",
|
||||
"aspect_ratio": "Aspect Ratio",
|
||||
"style_type": "Style",
|
||||
"rendering_speed": "Rendering Speed",
|
||||
"learn_more": "Learn More",
|
||||
"paint_course":"tutorial",
|
||||
"paint_course": "tutorial",
|
||||
"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",
|
||||
"image_file_required": "Please upload an image first",
|
||||
@@ -846,6 +846,29 @@
|
||||
"turbo": "Turbo",
|
||||
"quality": "Quality"
|
||||
},
|
||||
"quality_options": {
|
||||
"auto": "Auto",
|
||||
"low": "Low",
|
||||
"medium": "Medium",
|
||||
"high": "High"
|
||||
},
|
||||
"moderation_options": {
|
||||
"auto": "Auto",
|
||||
"low": "Low"
|
||||
},
|
||||
"background_options": {
|
||||
"auto": "Auto",
|
||||
"transparent": "Transparent",
|
||||
"opaque": "Opaque"
|
||||
},
|
||||
"aspect_ratios": {
|
||||
"square": "Square",
|
||||
"portrait": "Portrait",
|
||||
"landscape": "Landscape"
|
||||
},
|
||||
"quality": "Quality",
|
||||
"moderation": "Moderation",
|
||||
"background": "Background",
|
||||
"mode": {
|
||||
"generate": "Draw",
|
||||
"edit": "Edit",
|
||||
|
||||
@@ -820,12 +820,12 @@
|
||||
"seed_desc_tip": "同じシードとプロンプトで類似した画像を生成できますが、-1 に設定すると毎回異なる結果が生成されます",
|
||||
"title": "画像",
|
||||
"magic_prompt_option": "プロンプト強化",
|
||||
"model": "モデルバージョン",
|
||||
"model": "モデル",
|
||||
"aspect_ratio": "画幅比例",
|
||||
"style_type": "スタイル",
|
||||
"learn_more": "詳しくはこちら",
|
||||
"prompt_placeholder_edit": "画像の説明を入力します。テキスト描画には '二重引用符' を使用します",
|
||||
"paint_course":"チュートリアル",
|
||||
"paint_course": "チュートリアル",
|
||||
"proxy_required": "現在、プロキシを開く必要があります。これは、将来サポートされる予定です",
|
||||
"image_file_required": "画像を先にアップロードしてください",
|
||||
"image_file_retry": "画像を先にアップロードしてください",
|
||||
@@ -844,6 +844,29 @@
|
||||
"turbo": "高速",
|
||||
"quality": "高品質"
|
||||
},
|
||||
"quality_options": {
|
||||
"auto": "自動",
|
||||
"low": "低",
|
||||
"medium": "中",
|
||||
"high": "高"
|
||||
},
|
||||
"moderation_options": {
|
||||
"auto": "自動",
|
||||
"low": "低"
|
||||
},
|
||||
"background_options": {
|
||||
"auto": "自動",
|
||||
"transparent": "透明",
|
||||
"opaque": "不透明"
|
||||
},
|
||||
"aspect_ratios": {
|
||||
"square": "正方形",
|
||||
"portrait": "縦図",
|
||||
"landscape": "横図"
|
||||
},
|
||||
"quality": "品質",
|
||||
"moderation": "敏感度",
|
||||
"background": "背景",
|
||||
"mode": {
|
||||
"generate": "画像生成",
|
||||
"edit": "部分編集",
|
||||
|
||||
@@ -820,13 +820,13 @@
|
||||
"seed_desc_tip": "Одинаковые сиды и промпты могут генерировать похожие изображения, установка -1 будет создавать разные результаты каждый раз",
|
||||
"title": "Изображения",
|
||||
"magic_prompt_option": "Улучшение промпта",
|
||||
"model": "Версия",
|
||||
"model": "Модель",
|
||||
"aspect_ratio": "Пропорции изображения",
|
||||
"style_type": "Стиль",
|
||||
"rendering_speed": "Скорость рендеринга",
|
||||
"learn_more": "Узнать больше",
|
||||
"prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки",
|
||||
"paint_course":"Руководство / Учебник",
|
||||
"paint_course": "Руководство / Учебник",
|
||||
"proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение",
|
||||
"image_file_required": "Пожалуйста, сначала загрузите изображение",
|
||||
"image_file_retry": "Пожалуйста, сначала загрузите изображение",
|
||||
@@ -841,11 +841,34 @@
|
||||
"3d": "3D",
|
||||
"anime": "Аниме"
|
||||
},
|
||||
"quality_options": {
|
||||
"auto": "Авто",
|
||||
"low": "Низкое",
|
||||
"medium": "Среднее",
|
||||
"high": "Высокое"
|
||||
},
|
||||
"moderation_options": {
|
||||
"auto": "Авто",
|
||||
"low": "Низкое"
|
||||
},
|
||||
"background_options": {
|
||||
"auto": "Авто",
|
||||
"transparent": "Прозрачный",
|
||||
"opaque": "Непрозрачный"
|
||||
},
|
||||
"rendering_speeds": {
|
||||
"default": "По умолчанию",
|
||||
"turbo": "Быстро",
|
||||
"quality": "Качественно"
|
||||
},
|
||||
"aspect_ratios": {
|
||||
"square": "Квадрат",
|
||||
"portrait": "Портрет",
|
||||
"landscape": "Пейзаж"
|
||||
},
|
||||
"quality": "Качество",
|
||||
"moderation": "Сенсорность",
|
||||
"background": "Фон",
|
||||
"mode": {
|
||||
"generate": "Рисование",
|
||||
"edit": "Редактирование",
|
||||
@@ -892,7 +915,6 @@
|
||||
"seed_tip": "Контролирует случайный характер увеличения изображений для воспроизводимых результатов",
|
||||
"magic_prompt_option_tip": "Улучшает увеличение изображений с помощью интеллектуального оптимизирования промптов"
|
||||
},
|
||||
"rendering_speed": "Скорость рендеринга",
|
||||
"text_desc_required": "Пожалуйста, сначала введите описание изображения"
|
||||
},
|
||||
"prompts": {
|
||||
|
||||
@@ -820,12 +820,12 @@
|
||||
"seed_desc_tip": "相同的种子和提示词可以生成相似的图片,设置 -1 每次生成都不一样",
|
||||
"title": "图片",
|
||||
"magic_prompt_option": "提示词增强",
|
||||
"model": "版本",
|
||||
"model": "模型",
|
||||
"aspect_ratio": "画幅比例",
|
||||
"style_type": "风格",
|
||||
"rendering_speed": "渲染速度",
|
||||
"learn_more": "了解更多",
|
||||
"paint_course":"教程",
|
||||
"paint_course": "教程",
|
||||
"prompt_placeholder_edit": "输入你的图片描述,文本绘制用 \"双引号\" 包裹",
|
||||
"proxy_required": "目前需要打开代理才能查看生成图片,后续会支持国内直连",
|
||||
"image_file_required": "请先上传图片",
|
||||
@@ -846,11 +846,34 @@
|
||||
"turbo": "快速",
|
||||
"quality": "高质量"
|
||||
},
|
||||
"quality_options": {
|
||||
"auto": "自动",
|
||||
"low": "低",
|
||||
"medium": "中",
|
||||
"high": "高"
|
||||
},
|
||||
"moderation_options": {
|
||||
"auto": "自动",
|
||||
"low": "低"
|
||||
},
|
||||
"background_options": {
|
||||
"auto": "自动",
|
||||
"transparent": "透明",
|
||||
"opaque": "不透明"
|
||||
},
|
||||
"aspect_ratios": {
|
||||
"square": "方形",
|
||||
"portrait": "竖图",
|
||||
"landscape": "横图"
|
||||
},
|
||||
"quality": "质量",
|
||||
"moderation": "敏感度",
|
||||
"background": "背景",
|
||||
"mode": {
|
||||
"generate": "绘图",
|
||||
"edit": "编辑",
|
||||
"remix": "混合",
|
||||
"upscale": "放大"
|
||||
"upscale": "高清增强"
|
||||
},
|
||||
"generate": {
|
||||
"model_tip": "模型版本:V3 为最新版本,V2 为之前版本,V2A 为快速模型、V_1 为初代模型,_TURBO 为加速版本",
|
||||
|
||||
@@ -821,12 +821,12 @@
|
||||
"seed_desc_tip": "相同的種子和提示詞可以生成相似的圖片,設置 -1 每次生成都不一樣",
|
||||
"title": "繪圖",
|
||||
"magic_prompt_option": "提示詞增強",
|
||||
"model": "版本",
|
||||
"model": "模型",
|
||||
"aspect_ratio": "畫幅比例",
|
||||
"style_type": "風格",
|
||||
"learn_more": "了解更多",
|
||||
"prompt_placeholder_edit": "輸入你的圖片描述,文本繪製用 '雙引號' 包裹",
|
||||
"paint_course":"教程",
|
||||
"paint_course": "教程",
|
||||
"proxy_required": "目前需要打開代理才能查看生成圖片,後續會支持國內直連",
|
||||
"image_file_required": "請先上傳圖片",
|
||||
"image_file_retry": "請重新上傳圖片",
|
||||
@@ -846,6 +846,29 @@
|
||||
"turbo": "快速",
|
||||
"quality": "高品質"
|
||||
},
|
||||
"quality_options": {
|
||||
"auto": "自動",
|
||||
"low": "低",
|
||||
"medium": "中",
|
||||
"high": "高"
|
||||
},
|
||||
"moderation_options": {
|
||||
"auto": "自動",
|
||||
"low": "低"
|
||||
},
|
||||
"background_options": {
|
||||
"auto": "自動",
|
||||
"transparent": "透明",
|
||||
"opaque": "不透明"
|
||||
},
|
||||
"aspect_ratios": {
|
||||
"square": "方形",
|
||||
"portrait": "豎圖",
|
||||
"landscape": "橫圖"
|
||||
},
|
||||
"quality": "品質",
|
||||
"moderation": "敏感度",
|
||||
"background": "背景",
|
||||
"mode": {
|
||||
"generate": "繪圖",
|
||||
"edit": "編輯",
|
||||
|
||||
@@ -5,8 +5,6 @@ import { Col, Image, Row, Spin, Table } from 'antd'
|
||||
import React, { memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import GeminiFiles from './GeminiFiles'
|
||||
|
||||
interface ContentViewProps {
|
||||
id: FileTypes | 'all' | string
|
||||
files?: FileType[]
|
||||
@@ -45,10 +43,6 @@ const ContentView: React.FC<ContentViewProps> = ({ id, files, dataSource, column
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith('gemini_')) {
|
||||
return <GeminiFiles id={id.replace('gemini_', '') as string} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
|
||||
@@ -8,7 +8,6 @@ import React, { memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import FileItem from './FileItem'
|
||||
import GeminiFiles from './GeminiFiles'
|
||||
|
||||
interface FileItemProps {
|
||||
id: FileTypes | 'all' | string
|
||||
@@ -58,10 +57,6 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith('gemini_')) {
|
||||
return <GeminiFiles id={id.replace('gemini_', '') as string} />
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualList
|
||||
data={list}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import type { File } from '@google/genai'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { MB } from '@shared/config/constant'
|
||||
import { Spin } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import FileItem from './FileItem'
|
||||
|
||||
interface GeminiFilesProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
|
||||
const { provider } = useProvider(id)
|
||||
const [files, setFiles] = useState<File[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchFiles = useCallback(async () => {
|
||||
const files = await window.api.gemini.listFiles(provider.apiKey)
|
||||
files && setFiles(files.filter((file) => file.state === 'ACTIVE'))
|
||||
}, [provider])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
await fetchFiles()
|
||||
setLoading(false)
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch files:', error)
|
||||
window.message.error(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
})
|
||||
}, [fetchFiles])
|
||||
|
||||
useEffect(() => {
|
||||
setFiles([])
|
||||
}, [id])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Container>
|
||||
<LoadingWrapper>
|
||||
<Spin />
|
||||
</LoadingWrapper>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<FileListContainer>
|
||||
{files.map((file) => (
|
||||
<FileItem
|
||||
key={file.name}
|
||||
fileInfo={{
|
||||
name: file.displayName,
|
||||
ext: `.${file.name?.split('.').pop()}`,
|
||||
extra: `${dayjs(file.createTime).format('MM-DD HH:mm')} · ${(parseInt(file.sizeBytes || '0') / MB).toFixed(2)} MB`,
|
||||
actions: (
|
||||
<DeleteOutlined
|
||||
style={{ cursor: 'pointer', color: 'var(--color-error)' }}
|
||||
onClick={() => {
|
||||
setFiles(files.filter((f) => f.name !== file.name))
|
||||
window.api.gemini.deleteFile(file.name!, provider.apiKey).catch((error) => {
|
||||
console.error('Failed to delete file:', error)
|
||||
setFiles((prev) => [...prev, file])
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</FileListContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const FileListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
`
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 200px;
|
||||
`
|
||||
|
||||
export default GeminiFiles
|
||||
@@ -163,7 +163,7 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
|
||||
</Flex>
|
||||
)}
|
||||
{role === 'user' && !renderInputMessageAsMarkdown ? (
|
||||
<p className="markdown" style={{ marginBottom: 5 }}>
|
||||
<p className="markdown" style={{ marginBottom: 5, whiteSpace: 'pre-line' }}>
|
||||
{block.content}
|
||||
</p>
|
||||
) : (
|
||||
|
||||
@@ -22,17 +22,16 @@ import { Avatar, Button, Input, InputNumber, Radio, Segmented, Select, Slider, S
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { Info } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './Artboard'
|
||||
import { type ConfigItem, createModeConfigs } from './config/aihubmixConfig'
|
||||
import { DEFAULT_PAINTING } from './config/constants'
|
||||
import PaintingsList from './PaintingsList'
|
||||
import Artboard from './components/Artboard'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import { type ConfigItem, createModeConfigs, DEFAULT_PAINTING } from './config/aihubmixConfig'
|
||||
|
||||
// 使用函数创建配置项
|
||||
const modeConfigs = createModeConfigs()
|
||||
@@ -74,12 +73,13 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
{ label: t('paintings.mode.upscale'), value: 'upscale' }
|
||||
]
|
||||
|
||||
const getNewPainting = () => {
|
||||
const getNewPainting = useCallback(() => {
|
||||
return {
|
||||
...DEFAULT_PAINTING,
|
||||
model: mode === 'generate' ? 'gpt-image-1' : 'V_3',
|
||||
id: uuid()
|
||||
}
|
||||
}
|
||||
}, [mode])
|
||||
|
||||
const textareaRef = useRef<any>(null)
|
||||
|
||||
@@ -89,6 +89,47 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
updatePainting(mode, updatedPainting)
|
||||
}
|
||||
|
||||
const handleError = (error: unknown) => {
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
window.modal.error({
|
||||
content: getErrorMessage(error),
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const downloadImages = async (urls: string[]) => {
|
||||
const downloadedFiles = await Promise.all(
|
||||
urls.map(async (url) => {
|
||||
try {
|
||||
if (!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)
|
||||
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
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return downloadedFiles.filter((file): file is FileType => file !== null)
|
||||
}
|
||||
|
||||
const onGenerate = async () => {
|
||||
if (painting.files.length > 0) {
|
||||
const confirmed = await window.modal.confirm({
|
||||
@@ -129,11 +170,11 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
dispatch(setGenerating(true))
|
||||
|
||||
let body: string | FormData = ''
|
||||
const headers: Record<string, string> = {
|
||||
let headers: Record<string, string> = {
|
||||
'Api-Key': aihubmixProvider.apiKey
|
||||
}
|
||||
let url = aihubmixProvider.apiHost + `/ideogram/` + mode
|
||||
|
||||
// 不使用 AiProvider 的通用规则,而是直接调用自定义接口
|
||||
try {
|
||||
if (mode === 'generate') {
|
||||
if (painting.model === 'V_3') {
|
||||
@@ -214,67 +255,47 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
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)
|
||||
const validFiles = await downloadImages(urls)
|
||||
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
|
||||
})
|
||||
}
|
||||
handleError(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
setAbortController(null)
|
||||
}
|
||||
} else {
|
||||
// Existing V1/V2 API
|
||||
const requestData = {
|
||||
image_request: {
|
||||
let requestData: any = {}
|
||||
if (painting.model === 'gpt-image-1') {
|
||||
requestData = {
|
||||
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'
|
||||
size: painting.size === 'auto' ? undefined : painting.size,
|
||||
n: painting.n,
|
||||
quality: painting.quality,
|
||||
moderation: painting.moderation
|
||||
}
|
||||
url = aihubmixProvider.apiHost + `/v1/images/generations`
|
||||
headers = {
|
||||
Authorization: `Bearer ${aihubmixProvider.apiKey}`
|
||||
}
|
||||
} else {
|
||||
// Existing V1/V2 API
|
||||
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)
|
||||
@@ -352,37 +373,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
// 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)
|
||||
const validFiles = await downloadImages(urls)
|
||||
await FileManager.addFiles(validFiles)
|
||||
updatePaintingState({ files: validFiles, urls })
|
||||
}
|
||||
@@ -405,119 +396,6 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
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)
|
||||
}
|
||||
|
||||
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<string, any> = {
|
||||
prompt,
|
||||
model: painting.model,
|
||||
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({
|
||||
@@ -549,9 +427,9 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
// 只针对非V3模型使用通用接口
|
||||
if (!painting.model?.includes('V_3')) {
|
||||
if (!painting.model?.includes('V_3') || mode === 'upscale') {
|
||||
// 直接调用自定义接口
|
||||
const response = await fetch(`${aihubmixProvider.apiHost}/ideogram/${mode}`, { method: 'POST', headers, body })
|
||||
const response = await fetch(url, { method: 'POST', headers, body })
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
@@ -561,53 +439,27 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
const data = await response.json()
|
||||
console.log('通用API响应:', data)
|
||||
const urls = data.data.map((item) => item.url)
|
||||
const urls = data.data.filter((item) => item.url).map((item) => item.url)
|
||||
const base64s = data.data.filter((item) => item.b64_json).map((item) => item.b64_json)
|
||||
|
||||
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 = await downloadImages(urls)
|
||||
await FileManager.addFiles(validFiles)
|
||||
updatePaintingState({ files: validFiles, urls })
|
||||
}
|
||||
|
||||
if (base64s?.length > 0) {
|
||||
const validFiles = await Promise.all(
|
||||
base64s.map(async (base64) => {
|
||||
return await window.api.file.saveBase64Image(base64)
|
||||
})
|
||||
)
|
||||
|
||||
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
|
||||
|
||||
await FileManager.addFiles(validFiles)
|
||||
|
||||
updatePaintingState({ files: validFiles, urls })
|
||||
updatePaintingState({ files: validFiles, urls: validFiles.map((file) => file.name) })
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error && error.name !== 'AbortError') {
|
||||
window.modal.error({
|
||||
content: getErrorMessage(error),
|
||||
centered: true
|
||||
})
|
||||
}
|
||||
handleError(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
dispatch(setGenerating(false))
|
||||
@@ -617,43 +469,15 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
const handleRetry = async (painting: PaintingAction) => {
|
||||
setIsLoading(true)
|
||||
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
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const validFiles = downloadedFiles.filter((file): file is FileType => file !== null)
|
||||
|
||||
await FileManager.addFiles(validFiles)
|
||||
|
||||
updatePaintingState({ files: validFiles, urls: painting.urls })
|
||||
setIsLoading(false)
|
||||
try {
|
||||
const validFiles = await downloadImages(painting.urls)
|
||||
await FileManager.addFiles(validFiles)
|
||||
updatePaintingState({ files: validFiles, urls: painting.urls })
|
||||
} catch (error) {
|
||||
handleError(error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
@@ -754,20 +578,8 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
|
||||
// 渲染配置项的函数
|
||||
const renderConfigItem = (item: ConfigItem, index: number) => {
|
||||
const renderConfigForm = (item: ConfigItem) => {
|
||||
switch (item.type) {
|
||||
case 'title': {
|
||||
return (
|
||||
<SettingTitle key={index} style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
{t(item.title!)}
|
||||
{item.tooltip && (
|
||||
<Tooltip title={t(item.tooltip)}>
|
||||
<InfoIcon />
|
||||
</Tooltip>
|
||||
)}
|
||||
</SettingTitle>
|
||||
)
|
||||
}
|
||||
case 'select': {
|
||||
// 处理函数类型的disabled属性
|
||||
const isDisabled = typeof item.disabled === 'function' ? item.disabled(item, painting) : item.disabled
|
||||
@@ -786,10 +598,11 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
return (
|
||||
<Select
|
||||
key={index}
|
||||
style={{ width: '100%' }}
|
||||
listHeight={500}
|
||||
disabled={isDisabled}
|
||||
value={painting[item.key!] || item.initialValue}
|
||||
options={selectOptions}
|
||||
options={selectOptions as any}
|
||||
onChange={(v) => updatePaintingState({ [item.key!]: v })}
|
||||
/>
|
||||
)
|
||||
@@ -809,8 +622,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
|
||||
return (
|
||||
<Radio.Group
|
||||
key={index}
|
||||
value={painting[item.key!]}
|
||||
value={painting[item.key!] || item.initialValue}
|
||||
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}>
|
||||
{radioOptions!.map((option) => (
|
||||
<Radio.Button key={option.value} value={option.value}>
|
||||
@@ -822,96 +634,82 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
case 'slider': {
|
||||
return (
|
||||
<SliderContainer key={index}>
|
||||
<SliderContainer>
|
||||
<Slider
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
step={item.step}
|
||||
value={painting[item.key!] as number}
|
||||
value={(painting[item.key!] || item.initialValue) as number}
|
||||
onChange={(v) => updatePaintingState({ [item.key!]: v })}
|
||||
/>
|
||||
<StyledInputNumber
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
step={item.step}
|
||||
value={painting[item.key!] as number}
|
||||
value={(painting[item.key!] || item.initialValue) as number}
|
||||
onChange={(v) => updatePaintingState({ [item.key!]: v })}
|
||||
/>
|
||||
</SliderContainer>
|
||||
)
|
||||
}
|
||||
case 'input': {
|
||||
// 处理随机种子按钮的特殊情况
|
||||
if (item.key === 'seed') {
|
||||
return (
|
||||
<Input
|
||||
key={index}
|
||||
value={painting[item.key] as string}
|
||||
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
|
||||
suffix={
|
||||
<RedoOutlined onClick={handleRandomSeed} style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} />
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'input':
|
||||
return (
|
||||
<Input
|
||||
key={index}
|
||||
value={painting[item.key!] as string}
|
||||
value={(painting[item.key!] || item.initialValue) as string}
|
||||
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
|
||||
suffix={item.suffix}
|
||||
suffix={
|
||||
item.key === 'seed' ? (
|
||||
<RedoOutlined onClick={handleRandomSeed} style={{ cursor: 'pointer', color: 'var(--color-text-2)' }} />
|
||||
) : (
|
||||
item.suffix
|
||||
)
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'inputNumber': {
|
||||
case 'inputNumber':
|
||||
return (
|
||||
<InputNumber
|
||||
key={index}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
style={{ width: '100%' }}
|
||||
value={painting[item.key!] as number}
|
||||
value={(painting[item.key!] || item.initialValue) as number}
|
||||
onChange={(v) => updatePaintingState({ [item.key!]: v })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'textarea': {
|
||||
case 'textarea':
|
||||
return (
|
||||
<TextArea
|
||||
key={index}
|
||||
value={painting[item.key!] as string}
|
||||
value={(painting[item.key!] || item.initialValue) as string}
|
||||
onChange={(e) => updatePaintingState({ [item.key!]: e.target.value })}
|
||||
spellCheck={false}
|
||||
rows={4}
|
||||
/>
|
||||
)
|
||||
}
|
||||
case 'switch': {
|
||||
case 'switch':
|
||||
return (
|
||||
<HStack key={index}>
|
||||
<HStack>
|
||||
<Switch
|
||||
checked={painting[item.key!] as boolean}
|
||||
checked={(painting[item.key!] || item.initialValue) as boolean}
|
||||
onChange={(checked) => updatePaintingState({ [item.key!]: checked })}
|
||||
/>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
case 'image': {
|
||||
return (
|
||||
<ImageUploadButton
|
||||
key={index}
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
maxCount={1}
|
||||
showUploadList={false}
|
||||
listType="picture-card"
|
||||
onChange={async ({ file }) => {
|
||||
const path = file.originFileObj?.path || ''
|
||||
setFileMap({ ...fileMap, [path]: file.originFileObj as unknown as FileType })
|
||||
beforeUpload={(file) => {
|
||||
const path = URL.createObjectURL(file)
|
||||
setFileMap({ ...fileMap, [path]: file as unknown as FileType })
|
||||
updatePaintingState({ [item.key!]: path })
|
||||
return false // 阻止默认上传行为
|
||||
}}>
|
||||
{painting[item.key!] ? (
|
||||
<ImagePreview>
|
||||
<img src={'file://' + painting[item.key!]} alt="预览图" />
|
||||
<img src={painting[item.key!]} alt="预览图" />
|
||||
</ImagePreview>
|
||||
) : (
|
||||
<ImageSizeImage src={IcImageUp} theme={theme} />
|
||||
@@ -924,6 +722,23 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染配置项的函数
|
||||
const renderConfigItem = (item: ConfigItem, index: number) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<SettingTitle style={{ marginBottom: 5, marginTop: 15 }}>
|
||||
{t(item.title!)}
|
||||
{item.tooltip && (
|
||||
<Tooltip title={t(item.tooltip)}>
|
||||
<InfoIcon />
|
||||
</Tooltip>
|
||||
)}
|
||||
</SettingTitle>
|
||||
{renderConfigForm(item)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const onSelectPainting = (newPainting: PaintingAction) => {
|
||||
if (generating) return
|
||||
setPainting(newPainting)
|
||||
@@ -936,12 +751,13 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
addPainting(mode, newPainting)
|
||||
setPainting(newPainting)
|
||||
}
|
||||
}, [filteredPaintings, mode, addPainting, painting])
|
||||
}, [filteredPaintings, mode, addPainting, painting, getNewPainting])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = spaceClickTimer.current
|
||||
return () => {
|
||||
if (spaceClickTimer.current) {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
@@ -985,7 +801,7 @@ const AihubmixPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
</Select>
|
||||
|
||||
{/* 使用JSON配置渲染设置项 */}
|
||||
{modeConfigs[mode].map(renderConfigItem)}
|
||||
{modeConfigs[mode].filter((item) => (item.condition ? item.condition(painting) : true)).map(renderConfigItem)}
|
||||
</LeftContainer>
|
||||
<MainContainer>
|
||||
{/* 添加功能切换分段控制器 */}
|
||||
|
||||
@@ -26,7 +26,8 @@ import styled from 'styled-components'
|
||||
|
||||
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingHelpLink, SettingTitle } from '../settings'
|
||||
import Artboard from './Artboard'
|
||||
import Artboard from './components/Artboard'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
import {
|
||||
COURSE_URL,
|
||||
DEFAULT_PAINTING,
|
||||
@@ -34,7 +35,6 @@ import {
|
||||
STYLE_TYPE_OPTIONS,
|
||||
TEXT_TO_IMAGES_MODELS
|
||||
} from './config/DmxapiConfig'
|
||||
import PaintingsList from './PaintingsList'
|
||||
|
||||
const generateRandomSeed = () => Math.floor(Math.random() * 1000000).toString()
|
||||
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import { FC } from 'react'
|
||||
import { Route, Routes } from 'react-router-dom'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setDefaultPaintingProvider } from '@renderer/store/settings'
|
||||
import { PaintingProvider } from '@renderer/types'
|
||||
import { FC, useEffect } from 'react'
|
||||
import { Route, Routes, useParams } from 'react-router-dom'
|
||||
|
||||
import AihubmixPage from './AihubmixPage'
|
||||
import DmxapiPage from './DmxapiPage'
|
||||
import SiliconPage from './PaintingsPage'
|
||||
import SiliconPage from './SiliconPage'
|
||||
|
||||
const Options = ['aihubmix', 'silicon', 'dmxapi']
|
||||
|
||||
const PaintingsRoutePage: FC = () => {
|
||||
const params = useParams()
|
||||
const provider = params['*']
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
console.debug('defaultPaintingProvider', provider)
|
||||
if (provider && Options.includes(provider)) {
|
||||
dispatch(setDefaultPaintingProvider(provider as PaintingProvider))
|
||||
}
|
||||
}, [provider, dispatch])
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<AihubmixPage Options={Options} />} />
|
||||
<Route path="*" element={<AihubmixPage Options={Options} />} />
|
||||
<Route path="/aihubmix" element={<AihubmixPage Options={Options} />} />
|
||||
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
|
||||
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
|
||||
|
||||
+4
-4
@@ -35,8 +35,8 @@ import styled from 'styled-components'
|
||||
|
||||
import SendMessageButton from '../home/Inputbar/SendMessageButton'
|
||||
import { SettingTitle } from '../settings'
|
||||
import Artboard from './Artboard'
|
||||
import PaintingsList from './PaintingsList'
|
||||
import Artboard from './components/Artboard'
|
||||
import PaintingsList from './components/PaintingsList'
|
||||
|
||||
const IMAGE_SIZES = [
|
||||
{
|
||||
@@ -88,7 +88,7 @@ const DEFAULT_PAINTING: Painting = {
|
||||
|
||||
// let _painting: Painting
|
||||
|
||||
const PaintingsPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const SiliconPage: FC<{ Options: string[] }> = ({ Options }) => {
|
||||
const { t } = useTranslation()
|
||||
const { paintings, addPainting, removePainting, updatePainting } = usePaintings()
|
||||
const [painting, setPainting] = useState<Painting>(paintings[0] || DEFAULT_PAINTING)
|
||||
@@ -645,4 +645,4 @@ const StyledInputNumber = styled(InputNumber)`
|
||||
width: 70px;
|
||||
`
|
||||
|
||||
export default PaintingsPage
|
||||
export default SiliconPage
|
||||
+7
-7
@@ -7,7 +7,7 @@ import React, { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ImagePreview from '../home/Markdown/ImagePreview'
|
||||
import ImagePreview from '../../home/Markdown/ImagePreview'
|
||||
|
||||
interface ArtboardProps {
|
||||
painting: Painting
|
||||
@@ -98,8 +98,8 @@ const Artboard: FC<ArtboardProps> = ({
|
||||
{painting.urls.length > 0 && retry ? (
|
||||
<div>
|
||||
<ImageList>
|
||||
{painting.urls.map((url) => (
|
||||
<ImageListItem key={url}>{url}</ImageListItem>
|
||||
{painting.urls.map((url, index) => (
|
||||
<ImageListItem key={url || index}>{url}</ImageListItem>
|
||||
))}
|
||||
</ImageList>
|
||||
<div>
|
||||
@@ -109,11 +109,11 @@ const Artboard: FC<ArtboardProps> = ({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : imageCover ? (
|
||||
imageCover
|
||||
) : (
|
||||
imageCover ?
|
||||
imageCover:
|
||||
(<div>{t('paintings.image_placeholder')}</div>)
|
||||
)}
|
||||
<div>{t('paintings.image_placeholder')}</div>
|
||||
)}
|
||||
</ImagePlaceholder>
|
||||
)}
|
||||
{isLoading && (
|
||||
@@ -1,6 +1,14 @@
|
||||
import type { PaintingAction, PaintingsState } from '@renderer/types'
|
||||
import type { PaintingAction } from '@renderer/types'
|
||||
|
||||
import { ASPECT_RATIOS, RENDERING_SPEED_OPTIONS, STYLE_TYPES, V3_STYLE_TYPES } from './constants'
|
||||
import {
|
||||
ASPECT_RATIOS,
|
||||
BACKGROUND_OPTIONS,
|
||||
MODERATION_OPTIONS,
|
||||
QUALITY_OPTIONS,
|
||||
RENDERING_SPEED_OPTIONS,
|
||||
STYLE_TYPES,
|
||||
V3_STYLE_TYPES
|
||||
} from './constants'
|
||||
|
||||
// 配置项类型定义
|
||||
export type ConfigItem = {
|
||||
@@ -19,7 +27,14 @@ export type ConfigItem = {
|
||||
title?: string
|
||||
tooltip?: string
|
||||
options?:
|
||||
| Array<{ label: string; value: string | number; icon?: string; onlyV2?: boolean }>
|
||||
| Array<{
|
||||
label: string
|
||||
title?: string
|
||||
value?: string | number
|
||||
icon?: string
|
||||
onlyV2?: boolean
|
||||
options?: Array<{ label: string; value: string | number; icon?: string; onlyV2?: boolean }>
|
||||
}>
|
||||
| ((
|
||||
config: ConfigItem,
|
||||
painting: Partial<PaintingAction>
|
||||
@@ -32,189 +47,158 @@ export type ConfigItem = {
|
||||
disabled?: boolean | ((config: ConfigItem, painting: Partial<PaintingAction>) => boolean)
|
||||
initialValue?: string | number
|
||||
required?: boolean
|
||||
condition?: (painting: PaintingAction) => boolean
|
||||
}
|
||||
|
||||
export type AihubmixMode = keyof PaintingsState
|
||||
export type AihubmixMode = 'generate' | 'remix' | 'upscale'
|
||||
|
||||
// 创建配置项函数
|
||||
export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
return {
|
||||
paintings: [],
|
||||
DMXAPIPaintings: [],
|
||||
generate: [
|
||||
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.generate.model_tip' },
|
||||
{
|
||||
type: 'select',
|
||||
key: 'model',
|
||||
title: 'paintings.model',
|
||||
tooltip: 'paintings.generate.model_tip',
|
||||
options: [
|
||||
{ label: 'ideogram_V_3', value: 'V_3' },
|
||||
{ label: 'ideogram_V_2', value: 'V_2' },
|
||||
{ label: 'ideogram_V_2_TURBO', value: 'V_2_TURBO' },
|
||||
{ label: 'ideogram_V_2A', value: 'V_2A' },
|
||||
{ label: 'ideogram_V_2A_TURBO', value: 'V_2A_TURBO' },
|
||||
{ label: 'ideogram_V_1', value: 'V_1' },
|
||||
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
|
||||
{
|
||||
label: 'OpenAI',
|
||||
title: 'OpenAI',
|
||||
options: [{ label: 'gpt-image-1', value: 'gpt-image-1' }]
|
||||
},
|
||||
{
|
||||
label: 'ideogram',
|
||||
title: 'ideogram',
|
||||
options: [
|
||||
{ label: 'ideogram_V_3', value: 'V_3' },
|
||||
{ label: 'ideogram_V_2', value: 'V_2' },
|
||||
{ label: 'ideogram_V_2_TURBO', value: 'V_2_TURBO' },
|
||||
{ label: 'ideogram_V_2A', value: 'V_2A' },
|
||||
{ label: 'ideogram_V_2A_TURBO', value: 'V_2A_TURBO' },
|
||||
{ label: 'ideogram_V_1', value: 'V_1' },
|
||||
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ type: 'title', title: 'paintings.rendering_speed', tooltip: 'paintings.generate.rendering_speed_tip' },
|
||||
{
|
||||
type: 'select',
|
||||
key: 'renderingSpeed',
|
||||
title: 'paintings.rendering_speed',
|
||||
tooltip: 'paintings.generate.rendering_speed_tip',
|
||||
options: RENDERING_SPEED_OPTIONS,
|
||||
initialValue: 'DEFAULT',
|
||||
disabled: (_config, painting) => {
|
||||
const model = painting?.model
|
||||
return !model || !model.includes('V_3')
|
||||
}
|
||||
condition: (painting) => painting.model === 'V_3'
|
||||
},
|
||||
{ type: 'title', title: 'paintings.aspect_ratio' },
|
||||
{
|
||||
type: 'select',
|
||||
key: 'aspectRatio',
|
||||
options: ASPECT_RATIOS.map((size) => ({
|
||||
label: size.label,
|
||||
value: size.value,
|
||||
icon: size.icon
|
||||
}))
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.number_images',
|
||||
tooltip: 'paintings.generate.number_images_tip'
|
||||
title: 'paintings.aspect_ratio',
|
||||
options: ASPECT_RATIOS,
|
||||
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
key: 'numImages',
|
||||
title: 'paintings.number_images',
|
||||
tooltip: 'paintings.generate.number_images_tip',
|
||||
min: 1,
|
||||
max: 8
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.style_type',
|
||||
tooltip: 'paintings.generate.style_type_tip'
|
||||
max: 8,
|
||||
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
key: 'styleType',
|
||||
title: 'paintings.style_type',
|
||||
tooltip: 'paintings.generate.style_type_tip',
|
||||
options: (_config, painting) => {
|
||||
// 根据模型选择显示不同的样式类型选项
|
||||
return painting?.model?.includes('V_3') ? V3_STYLE_TYPES : STYLE_TYPES
|
||||
},
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.seed',
|
||||
tooltip: 'paintings.generate.seed_tip'
|
||||
disabled: false,
|
||||
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
key: 'seed'
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.negative_prompt',
|
||||
tooltip: 'paintings.generate.negative_prompt_tip'
|
||||
key: 'seed',
|
||||
title: 'paintings.seed',
|
||||
tooltip: 'paintings.generate.seed_tip',
|
||||
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
|
||||
},
|
||||
{
|
||||
type: 'textarea',
|
||||
key: 'negativePrompt'
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.magic_prompt_option',
|
||||
tooltip: 'paintings.generate.magic_prompt_option_tip'
|
||||
key: 'negativePrompt',
|
||||
title: 'paintings.negative_prompt',
|
||||
tooltip: 'paintings.generate.negative_prompt_tip',
|
||||
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
key: 'magicPromptOption'
|
||||
}
|
||||
],
|
||||
edit: [
|
||||
{ type: 'title', title: 'paintings.edit.image_file' },
|
||||
{
|
||||
type: 'image',
|
||||
key: 'imageFile'
|
||||
key: 'magicPromptOption',
|
||||
title: 'paintings.magic_prompt_option',
|
||||
tooltip: 'paintings.generate.magic_prompt_option_tip',
|
||||
condition: (painting) => Boolean(painting.model?.startsWith('V_'))
|
||||
},
|
||||
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.edit.model_tip' },
|
||||
{
|
||||
type: 'select',
|
||||
key: 'model',
|
||||
key: 'size',
|
||||
title: 'paintings.aspect_ratio',
|
||||
options: [
|
||||
{ label: 'ideogram_V_3', value: 'V_3' },
|
||||
{ label: 'ideogram_V_2', value: 'V_2' },
|
||||
{ label: 'ideogram_V_2_TURBO', value: 'V_2_TURBO' },
|
||||
{ label: 'ideogram_V_2A', value: 'V_2A' },
|
||||
{ label: 'ideogram_V_2A_TURBO', value: 'V_2A_TURBO' },
|
||||
{ label: 'ideogram_V_1', value: 'V_1' },
|
||||
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
|
||||
]
|
||||
},
|
||||
{ type: 'title', title: 'paintings.rendering_speed', tooltip: 'paintings.edit.rendering_speed_tip' },
|
||||
{
|
||||
type: 'select',
|
||||
key: 'renderingSpeed',
|
||||
options: RENDERING_SPEED_OPTIONS,
|
||||
initialValue: 'DEFAULE',
|
||||
disabled: (_config, painting) => {
|
||||
const model = painting?.model
|
||||
return !model || !model.includes('V_3')
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.number_images',
|
||||
tooltip: 'paintings.edit.number_images_tip'
|
||||
{ label: '自动', value: 'auto' },
|
||||
{ label: '1:1', value: '1024x1024' },
|
||||
{ label: '3:2', value: '1536x1024' },
|
||||
{ label: '2:3', value: '1024x1536' }
|
||||
],
|
||||
initialValue: '1024x1024',
|
||||
condition: (painting) => painting.model === 'gpt-image-1'
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
key: 'numImages',
|
||||
key: 'n',
|
||||
title: 'paintings.number_images',
|
||||
tooltip: 'paintings.generate.number_images_tip',
|
||||
min: 1,
|
||||
max: 8
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.style_type',
|
||||
tooltip: 'paintings.edit.style_type_tip'
|
||||
max: 10,
|
||||
initialValue: 1,
|
||||
condition: (painting) => painting.model === 'gpt-image-1'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
key: 'styleType',
|
||||
options: (_config, painting) => {
|
||||
// 根据模型选择显示不同的样式类型选项
|
||||
return painting?.model?.includes('V_3') ? V3_STYLE_TYPES : STYLE_TYPES
|
||||
},
|
||||
disabled: false
|
||||
key: 'quality',
|
||||
title: 'paintings.quality',
|
||||
options: QUALITY_OPTIONS,
|
||||
initialValue: 'auto',
|
||||
condition: (painting) => painting.model === 'gpt-image-1'
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.seed',
|
||||
tooltip: 'paintings.edit.seed_tip'
|
||||
type: 'select',
|
||||
key: 'moderation',
|
||||
title: 'paintings.moderation',
|
||||
options: MODERATION_OPTIONS,
|
||||
initialValue: 'auto',
|
||||
condition: (painting) => painting.model === 'gpt-image-1'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
key: 'seed'
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.magic_prompt_option',
|
||||
tooltip: 'paintings.edit.magic_prompt_option_tip'
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
key: 'magicPromptOption'
|
||||
type: 'select',
|
||||
key: 'background',
|
||||
title: 'paintings.background',
|
||||
options: BACKGROUND_OPTIONS,
|
||||
initialValue: 'auto',
|
||||
condition: (painting) => painting.model === 'gpt-image-1'
|
||||
}
|
||||
],
|
||||
remix: [
|
||||
{ type: 'title', title: 'paintings.remix.image_file' },
|
||||
{
|
||||
type: 'image',
|
||||
key: 'imageFile'
|
||||
key: 'imageFile',
|
||||
title: 'paintings.remix.image_file'
|
||||
},
|
||||
{ type: 'title', title: 'paintings.model', tooltip: 'paintings.remix.model_tip' },
|
||||
{
|
||||
type: 'select',
|
||||
key: 'model',
|
||||
title: 'paintings.model',
|
||||
tooltip: 'paintings.remix.model_tip',
|
||||
options: [
|
||||
{ label: 'ideogram_V_3', value: 'V_3' },
|
||||
{ label: 'ideogram_V_2', value: 'V_2' },
|
||||
@@ -225,10 +209,10 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
{ label: 'ideogram_V_1_TURBO', value: 'V_1_TURBO' }
|
||||
]
|
||||
},
|
||||
{ type: 'title', title: 'paintings.rendering_speed', tooltip: 'paintings.remix.rendering_speed_tip' },
|
||||
{
|
||||
type: 'select',
|
||||
key: 'renderingSpeed',
|
||||
title: 'paintings.rendering_speed',
|
||||
options: RENDERING_SPEED_OPTIONS,
|
||||
initialValue: 'DEFAULT',
|
||||
disabled: (_config, painting) => {
|
||||
@@ -236,42 +220,32 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
return !model || !model.includes('V_3')
|
||||
}
|
||||
},
|
||||
{ type: 'title', title: 'paintings.aspect_ratio' },
|
||||
{
|
||||
type: 'select',
|
||||
key: 'aspectRatio',
|
||||
options: ASPECT_RATIOS.map((size) => ({
|
||||
label: size.label,
|
||||
value: size.value,
|
||||
icon: size.icon
|
||||
}))
|
||||
title: 'paintings.aspect_ratio',
|
||||
options: ASPECT_RATIOS
|
||||
},
|
||||
{ type: 'title', title: 'paintings.remix.image_weight' },
|
||||
{
|
||||
type: 'slider',
|
||||
key: 'imageWeight',
|
||||
title: 'paintings.remix.image_weight',
|
||||
min: 1,
|
||||
max: 100
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.number_images',
|
||||
tooltip: 'paintings.remix.number_images_tip'
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
key: 'numImages',
|
||||
title: 'paintings.number_images',
|
||||
tooltip: 'paintings.remix.number_images_tip',
|
||||
min: 1,
|
||||
max: 8
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.style_type',
|
||||
tooltip: 'paintings.remix.style_type_tip'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
key: 'styleType',
|
||||
title: 'paintings.style_type',
|
||||
tooltip: 'paintings.remix.style_type_tip',
|
||||
options: (_config, painting) => {
|
||||
// 根据模型选择显示不同的样式类型选项
|
||||
return painting?.model?.includes('V_3') ? V3_STYLE_TYPES : STYLE_TYPES
|
||||
@@ -279,78 +253,92 @@ export const createModeConfigs = (): Record<AihubmixMode, ConfigItem[]> => {
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
type: 'input',
|
||||
key: 'seed',
|
||||
title: 'paintings.seed',
|
||||
tooltip: 'paintings.remix.seed_tip'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
key: 'seed'
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
type: 'textarea',
|
||||
key: 'negativePrompt',
|
||||
title: 'paintings.negative_prompt',
|
||||
tooltip: 'paintings.remix.negative_prompt_tip'
|
||||
},
|
||||
{
|
||||
type: 'textarea',
|
||||
key: 'negativePrompt'
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
type: 'switch',
|
||||
key: 'magicPromptOption',
|
||||
title: 'paintings.magic_prompt_option',
|
||||
tooltip: 'paintings.remix.magic_prompt_option_tip'
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
key: 'magicPromptOption'
|
||||
}
|
||||
],
|
||||
upscale: [
|
||||
{ type: 'title', title: 'paintings.upscale.image_file' },
|
||||
{
|
||||
type: 'image',
|
||||
key: 'imageFile',
|
||||
title: 'paintings.upscale.image_file',
|
||||
required: true
|
||||
},
|
||||
{ type: 'title', title: 'paintings.upscale.resemblance', tooltip: 'paintings.upscale.resemblance_tip' },
|
||||
{ type: 'slider', key: 'resemblance', min: 1, max: 100 },
|
||||
{ type: 'title', title: 'paintings.upscale.detail', tooltip: 'paintings.upscale.detail_tip' },
|
||||
{
|
||||
type: 'slider',
|
||||
key: 'detail',
|
||||
key: 'resemblance',
|
||||
title: 'paintings.upscale.resemblance',
|
||||
min: 1,
|
||||
max: 100
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
title: 'paintings.number_images',
|
||||
tooltip: 'paintings.upscale.number_images_tip'
|
||||
type: 'slider',
|
||||
key: 'detail',
|
||||
title: 'paintings.upscale.detail',
|
||||
tooltip: 'paintings.upscale.detail_tip',
|
||||
min: 1,
|
||||
max: 100
|
||||
},
|
||||
{
|
||||
type: 'slider',
|
||||
key: 'numImages',
|
||||
title: 'paintings.number_images',
|
||||
tooltip: 'paintings.upscale.number_images_tip',
|
||||
min: 1,
|
||||
max: 8
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
type: 'input',
|
||||
key: 'seed',
|
||||
title: 'paintings.seed',
|
||||
tooltip: 'paintings.upscale.seed_tip'
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
key: 'seed'
|
||||
},
|
||||
{
|
||||
type: 'title',
|
||||
type: 'switch',
|
||||
key: 'magicPromptOption',
|
||||
title: 'paintings.magic_prompt_option',
|
||||
tooltip: 'paintings.upscale.magic_prompt_option_tip'
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
key: 'magicPromptOption'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// 几种默认的绘画配置
|
||||
export const DEFAULT_PAINTING: PaintingAction = {
|
||||
id: 'aihubmix_1',
|
||||
model: 'gpt-image-1',
|
||||
aspectRatio: 'ASPECT_1_1',
|
||||
numImages: 1,
|
||||
styleType: 'AUTO',
|
||||
prompt: '',
|
||||
negativePrompt: '',
|
||||
magicPromptOption: true,
|
||||
seed: '',
|
||||
imageWeight: 50,
|
||||
resemblance: 50,
|
||||
detail: 50,
|
||||
imageFile: undefined,
|
||||
mask: undefined,
|
||||
files: [],
|
||||
urls: [],
|
||||
renderingSpeed: 'DEFAULT',
|
||||
size: '1024x1024',
|
||||
background: 'auto',
|
||||
quality: 'auto',
|
||||
moderation: 'auto',
|
||||
n: 1
|
||||
}
|
||||
|
||||
@@ -1,87 +1,78 @@
|
||||
import ImageSize1_1 from '@renderer/assets/images/paintings/image-size-1-1.svg'
|
||||
import ImageSize1_2 from '@renderer/assets/images/paintings/image-size-1-2.svg'
|
||||
import ImageSize3_2 from '@renderer/assets/images/paintings/image-size-3-2.svg'
|
||||
import ImageSize3_4 from '@renderer/assets/images/paintings/image-size-3-4.svg'
|
||||
import ImageSize9_16 from '@renderer/assets/images/paintings/image-size-9-16.svg'
|
||||
import ImageSize16_9 from '@renderer/assets/images/paintings/image-size-16-9.svg'
|
||||
import type { PaintingAction } from '@renderer/types'
|
||||
|
||||
// 几种默认的绘画配置
|
||||
export const DEFAULT_PAINTING: PaintingAction = {
|
||||
id: 'aihubmix_1',
|
||||
model: 'V_3',
|
||||
aspectRatio: 'ASPECT_1_1',
|
||||
numImages: 1,
|
||||
styleType: 'AUTO',
|
||||
prompt: '',
|
||||
negativePrompt: '',
|
||||
magicPromptOption: true,
|
||||
seed: '',
|
||||
imageWeight: 50,
|
||||
resemblance: 50,
|
||||
detail: 50,
|
||||
imageFile: undefined,
|
||||
mask: undefined,
|
||||
files: [],
|
||||
urls: [],
|
||||
renderingSpeed: 'DEFAULT'
|
||||
}
|
||||
|
||||
export const ASPECT_RATIOS = [
|
||||
{
|
||||
label: '1:1',
|
||||
value: 'ASPECT_1_1',
|
||||
icon: ImageSize1_1
|
||||
label: 'paintings.aspect_ratios.square',
|
||||
options: [
|
||||
{
|
||||
label: '1:1',
|
||||
value: 'ASPECT_1_1'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '3:1',
|
||||
value: 'ASPECT_3_1',
|
||||
icon: ImageSize3_2
|
||||
label: 'paintings.aspect_ratios.landscape',
|
||||
options: [
|
||||
{
|
||||
label: '1:2',
|
||||
value: 'ASPECT_1_2'
|
||||
},
|
||||
{
|
||||
label: '1:3',
|
||||
value: 'ASPECT_1_3'
|
||||
},
|
||||
{
|
||||
label: '2:3',
|
||||
value: 'ASPECT_2_3'
|
||||
},
|
||||
{
|
||||
label: '3:4',
|
||||
value: 'ASPECT_3_4'
|
||||
},
|
||||
{
|
||||
label: '4:5',
|
||||
value: 'ASPECT_4_5'
|
||||
},
|
||||
{
|
||||
label: '9:16',
|
||||
value: 'ASPECT_9_16'
|
||||
},
|
||||
{
|
||||
label: '10:16',
|
||||
value: 'ASPECT_10_16'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '1:3',
|
||||
value: 'ASPECT_1_3',
|
||||
icon: ImageSize1_2
|
||||
},
|
||||
{
|
||||
label: '3:2',
|
||||
value: 'ASPECT_3_2',
|
||||
icon: ImageSize3_2
|
||||
},
|
||||
{
|
||||
label: '2:3',
|
||||
value: 'ASPECT_2_3',
|
||||
icon: ImageSize1_2
|
||||
},
|
||||
{
|
||||
label: '4:3',
|
||||
value: 'ASPECT_4_3',
|
||||
icon: ImageSize3_4
|
||||
},
|
||||
{
|
||||
label: '3:4',
|
||||
value: 'ASPECT_3_4',
|
||||
icon: ImageSize3_4
|
||||
},
|
||||
{
|
||||
label: '16:9',
|
||||
value: 'ASPECT_16_9',
|
||||
icon: ImageSize16_9
|
||||
},
|
||||
{
|
||||
label: '9:16',
|
||||
value: 'ASPECT_9_16',
|
||||
icon: ImageSize9_16
|
||||
},
|
||||
{
|
||||
label: '16:10',
|
||||
value: 'ASPECT_16_10',
|
||||
icon: ImageSize16_9
|
||||
},
|
||||
{
|
||||
label: '10:16',
|
||||
value: 'ASPECT_10_16',
|
||||
icon: ImageSize9_16
|
||||
label: 'paintings.aspect_ratios.landscape',
|
||||
options: [
|
||||
{
|
||||
label: '2:1',
|
||||
value: 'ASPECT_2_1'
|
||||
},
|
||||
{
|
||||
label: '3:1',
|
||||
value: 'ASPECT_3_1'
|
||||
},
|
||||
{
|
||||
label: '3:2',
|
||||
value: 'ASPECT_3_2'
|
||||
},
|
||||
{
|
||||
label: '4:3',
|
||||
value: 'ASPECT_4_3'
|
||||
},
|
||||
{
|
||||
label: '5:4',
|
||||
value: 'ASPECT_5_4'
|
||||
},
|
||||
{
|
||||
label: '16:9',
|
||||
value: 'ASPECT_16_9'
|
||||
},
|
||||
{
|
||||
label: '16:10',
|
||||
value: 'ASPECT_16_10'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -132,3 +123,21 @@ export const RENDERING_SPEED_OPTIONS = [
|
||||
value: 'QUALITY'
|
||||
}
|
||||
]
|
||||
|
||||
export const QUALITY_OPTIONS = [
|
||||
{ label: 'paintings.quality_options.auto', value: 'auto' },
|
||||
{ label: 'paintings.quality_options.low', value: 'low' },
|
||||
{ label: 'paintings.quality_options.medium', value: 'medium' },
|
||||
{ label: 'paintings.quality_options.high', value: 'high' }
|
||||
]
|
||||
|
||||
export const MODERATION_OPTIONS = [
|
||||
{ label: 'paintings.moderation_options.auto', value: 'auto' },
|
||||
{ label: 'paintings.moderation_options.low', value: 'low' }
|
||||
]
|
||||
|
||||
export const BACKGROUND_OPTIONS = [
|
||||
{ label: 'paintings.background_options.auto', value: 'auto' },
|
||||
{ label: 'paintings.background_options.transparent', value: 'transparent' },
|
||||
{ label: 'paintings.background_options.opaque', value: 'opaque' }
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Content,
|
||||
File,
|
||||
FileState,
|
||||
FinishReason,
|
||||
FunctionCall,
|
||||
GenerateContentConfig,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
HarmBlockThreshold,
|
||||
HarmCategory,
|
||||
Modality,
|
||||
Pager,
|
||||
Part,
|
||||
PartUnion,
|
||||
SafetySetting,
|
||||
@@ -27,6 +29,7 @@ import {
|
||||
import { getStoreSetting } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/AssistantService'
|
||||
import { CacheService } from '@renderer/services/CacheService'
|
||||
import { EVENT_NAMES } from '@renderer/services/EventService'
|
||||
import {
|
||||
filterContextMessages,
|
||||
@@ -91,7 +94,7 @@ export default class GeminiProvider extends BaseProvider {
|
||||
const isSmallFile = file.size < smallFileSize
|
||||
|
||||
if (isSmallFile) {
|
||||
const { data, mimeType } = await window.api.gemini.base64File(file)
|
||||
const { data, mimeType } = await this.base64File(file)
|
||||
return {
|
||||
inlineData: {
|
||||
data,
|
||||
@@ -101,7 +104,7 @@ export default class GeminiProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
// Retrieve file from Gemini uploaded files
|
||||
const fileMetadata: File | undefined = await window.api.gemini.retrieveFile(file, this.apiKey)
|
||||
const fileMetadata: File | undefined = await this.retrieveFile(file)
|
||||
|
||||
if (fileMetadata) {
|
||||
return {
|
||||
@@ -113,10 +116,7 @@ export default class GeminiProvider extends BaseProvider {
|
||||
}
|
||||
|
||||
// If file is not found, upload it to Gemini
|
||||
const result = await window.api.gemini.uploadFile(file, {
|
||||
apiKey: this.apiKey,
|
||||
baseURL: this.getBaseURL()
|
||||
})
|
||||
const result = await this.uploadFile(file)
|
||||
|
||||
return {
|
||||
fileData: {
|
||||
@@ -1153,4 +1153,62 @@ export default class GeminiProvider extends BaseProvider {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
private async uploadFile(file: FileType): Promise<File> {
|
||||
return await this.sdk.files.upload({
|
||||
file: file.path,
|
||||
config: {
|
||||
mimeType: 'application/pdf',
|
||||
name: file.id,
|
||||
displayName: file.origin_name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async base64File(file: FileType) {
|
||||
const { data } = await window.api.file.base64File(file.id + file.ext)
|
||||
return {
|
||||
data,
|
||||
mimeType: 'application/pdf'
|
||||
}
|
||||
}
|
||||
|
||||
private async retrieveFile(file: FileType): Promise<File | undefined> {
|
||||
const cachedResponse = CacheService.get<any>('gemini_file_list')
|
||||
|
||||
if (cachedResponse) {
|
||||
return this.processResponse(cachedResponse, file)
|
||||
}
|
||||
|
||||
const response = await this.sdk.files.list()
|
||||
CacheService.set('gemini_file_list', response, 3000)
|
||||
|
||||
return this.processResponse(response, file)
|
||||
}
|
||||
|
||||
private async processResponse(response: Pager<File>, file: FileType) {
|
||||
for await (const f of response) {
|
||||
if (f.state === FileState.ACTIVE) {
|
||||
if (f.displayName === file.origin_name && Number(f.sizeBytes) === file.size) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
// @ts-ignore unused
|
||||
private async listFiles(): Promise<File[]> {
|
||||
const files: File[] = []
|
||||
for await (const f of await this.sdk.files.list()) {
|
||||
files.push(f)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// @ts-ignore unused
|
||||
private async deleteFile(fileId: string) {
|
||||
await this.sdk.files.delete({ name: fileId })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WebSearchState } from '@renderer/store/websearch'
|
||||
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
||||
import { BochaSearchParams, BochaSearchResponse } from '@renderer/types/bocha'
|
||||
import { BochaSearchParams, BochaSearchResponse } from '@renderer/utils/bocha'
|
||||
|
||||
import BaseWebSearchProvider from './BaseWebSearchProvider'
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
interface CacheItem<T> {
|
||||
data: T
|
||||
timestamp: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
export class CacheService {
|
||||
private static cache: Map<string, CacheItem<any>> = new Map()
|
||||
|
||||
/**
|
||||
* Set cache
|
||||
* @param key Cache key
|
||||
* @param data Cache data
|
||||
* @param duration Cache duration (in milliseconds)
|
||||
*/
|
||||
static set<T>(key: string, data: T, duration: number): void {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
duration
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache
|
||||
* @param key Cache key
|
||||
* @returns Returns data if cache exists and not expired, otherwise returns null
|
||||
*/
|
||||
static get<T>(key: string): T | null {
|
||||
const item = this.cache.get(key)
|
||||
if (!item) return null
|
||||
|
||||
const now = Date.now()
|
||||
if (now - item.timestamp > item.duration) {
|
||||
this.remove(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return item.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specific cache
|
||||
* @param key Cache key
|
||||
*/
|
||||
static remove(key: string): void {
|
||||
this.cache.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*/
|
||||
static clear(): void {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache exists and is valid
|
||||
* @param key Cache key
|
||||
* @returns boolean
|
||||
*/
|
||||
static has(key: string): boolean {
|
||||
const item = this.cache.get(key)
|
||||
if (!item) return false
|
||||
|
||||
const now = Date.now()
|
||||
if (now - item.timestamp > item.duration) {
|
||||
this.remove(key)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,22 @@ class FileManager {
|
||||
return fileData.data
|
||||
}
|
||||
|
||||
static async addBase64File(file: FileType): Promise<FileType> {
|
||||
Logger.log(`[FileManager] Adding base64 file: ${JSON.stringify(file)}`)
|
||||
|
||||
const base64File = await window.api.file.base64File(file.id + file.ext)
|
||||
const fileRecord = await db.files.get(base64File.id)
|
||||
|
||||
if (fileRecord) {
|
||||
await db.files.update(fileRecord.id, { ...fileRecord, count: fileRecord.count + 1 })
|
||||
return fileRecord
|
||||
}
|
||||
|
||||
await db.files.add(base64File)
|
||||
|
||||
return base64File
|
||||
}
|
||||
|
||||
static async uploadFile(file: FileType): Promise<FileType> {
|
||||
Logger.log(`[FileManager] Uploading file: ${JSON.stringify(file)}`)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MathEngine,
|
||||
OpenAIServiceTier,
|
||||
OpenAISummaryText,
|
||||
PaintingProvider,
|
||||
ThemeMode,
|
||||
TranslateLanguageVarious
|
||||
} from '@renderer/types'
|
||||
@@ -167,6 +168,7 @@ export interface SettingsState {
|
||||
backup: boolean
|
||||
knowledgeEmbed: boolean
|
||||
}
|
||||
defaultPaintingProvider: PaintingProvider
|
||||
}
|
||||
|
||||
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||
@@ -298,7 +300,8 @@ export const initialState: SettingsState = {
|
||||
assistant: false,
|
||||
backup: false,
|
||||
knowledgeEmbed: false
|
||||
}
|
||||
},
|
||||
defaultPaintingProvider: 'aihubmix'
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@@ -632,6 +635,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setNotificationSettings: (state, action: PayloadAction<SettingsState['notification']>) => {
|
||||
state.notification = action.payload
|
||||
},
|
||||
setDefaultPaintingProvider: (state, action: PayloadAction<PaintingProvider>) => {
|
||||
state.defaultPaintingProvider = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -730,7 +736,8 @@ export const {
|
||||
setEnableBackspaceDeleteModel,
|
||||
setOpenAISummaryText,
|
||||
setOpenAIServiceTier,
|
||||
setNotificationSettings
|
||||
setNotificationSettings,
|
||||
setDefaultPaintingProvider
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { WebSearchResultBlock } from '@anthropic-ai/sdk/resources'
|
||||
import type { GroundingMetadata } from '@google/genai'
|
||||
import type OpenAI from 'openai'
|
||||
import React from 'react'
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
import type { Message } from './newMessage'
|
||||
|
||||
@@ -185,6 +185,8 @@ export type PaintingParams = {
|
||||
files: FileType[]
|
||||
}
|
||||
|
||||
export type PaintingProvider = 'aihubmix' | 'silicon' | 'dmxapi'
|
||||
|
||||
export interface Painting extends PaintingParams {
|
||||
model?: string
|
||||
prompt?: string
|
||||
@@ -207,6 +209,11 @@ export interface GeneratePainting extends PaintingParams {
|
||||
negativePrompt?: string
|
||||
magicPromptOption?: boolean
|
||||
renderingSpeed?: string
|
||||
quality?: string
|
||||
moderation?: string
|
||||
n?: number
|
||||
size?: string
|
||||
background?: string
|
||||
}
|
||||
|
||||
export interface EditPainting extends PaintingParams {
|
||||
@@ -274,7 +281,7 @@ export type MinAppType = {
|
||||
url: string
|
||||
bodered?: boolean
|
||||
background?: string
|
||||
style?: React.CSSProperties
|
||||
style?: CSSProperties
|
||||
addTime?: string
|
||||
type?: 'Custom' | 'Default' // Added the 'type' property
|
||||
}
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const InputType = z.enum(['text', 'image', 'audio', 'video', 'document'])
|
||||
export type InputType = z.infer<typeof InputType>
|
||||
|
||||
export const OutputType = z.enum(['text', 'image', 'audio', 'video', 'vector'])
|
||||
export type OutputType = z.infer<typeof OutputType>
|
||||
|
||||
export const OutputMode = z.enum(['sync', 'streaming'])
|
||||
export type OutputMode = z.infer<typeof OutputMode>
|
||||
|
||||
export const ModelCapability = z.enum([
|
||||
'audioGeneration',
|
||||
'cache',
|
||||
'codeExecution',
|
||||
'embedding',
|
||||
'fineTuning',
|
||||
'imageGeneration',
|
||||
'OCR',
|
||||
'realTime',
|
||||
'rerank',
|
||||
'reasoning',
|
||||
'streaming',
|
||||
'structuredOutput',
|
||||
'textGeneration',
|
||||
'translation',
|
||||
'transcription',
|
||||
'toolUse',
|
||||
'videoGeneration',
|
||||
'webSearch'
|
||||
])
|
||||
export type ModelCapability = z.infer<typeof ModelCapability>
|
||||
|
||||
export const ModelSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
modelId: z.string(),
|
||||
providerId: z.string(),
|
||||
name: z.string(),
|
||||
group: z.string(),
|
||||
description: z.string().optional(),
|
||||
owned_by: z.string().optional(),
|
||||
|
||||
supportedInputs: z.array(InputType),
|
||||
supportedOutputs: z.array(OutputType),
|
||||
supportedOutputModes: z.array(OutputMode),
|
||||
|
||||
limits: z
|
||||
.object({
|
||||
inputTokenLimit: z.number().optional(),
|
||||
outputTokenLimit: z.number().optional(),
|
||||
contextWindow: z.number().optional()
|
||||
})
|
||||
.optional(),
|
||||
|
||||
price: z
|
||||
.object({
|
||||
inputTokenPrice: z.number().optional(),
|
||||
outputTokenPrice: z.number().optional()
|
||||
})
|
||||
.optional(),
|
||||
|
||||
capabilities: z.array(ModelCapability)
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// 如果模型支持streaming,则必须支持streamingOutputMode
|
||||
if (data.capabilities.includes('streaming') && !data.supportedOutputModes.includes('streaming')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果模型有OCR能力,则必须支持图像输入类型或者文件输入类型
|
||||
if (
|
||||
data.capabilities.includes('OCR') &&
|
||||
!data.supportedInputs.includes('image') &&
|
||||
!data.supportedInputs.includes('document')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果模型有图像生成能力,则必须支持图像输出
|
||||
if (data.capabilities.includes('imageGeneration') && !data.supportedOutputs.includes('image')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果有音频生成能力,则必须支持音频输出类型
|
||||
if (data.capabilities.includes('audioGeneration') && !data.supportedOutputs.includes('audio')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果有音频识别能力,则必须支持音频输入类型
|
||||
if (
|
||||
(data.capabilities.includes('transcription') || data.capabilities.includes('translation')) &&
|
||||
!data.supportedInputs.includes('audio')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果有视频生成能力,则必须支持视频输出类型
|
||||
if (data.capabilities.includes('videoGeneration') && !data.supportedOutputs.includes('video')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果模型有embedding能力,则必须支持向量输出类型
|
||||
if (data.capabilities.includes('embedding') && !data.supportedOutputs.includes('vector')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果模型有toolUse, Reasoning, streaming, cache, codeExecution, imageGeneration, audioGeneration, videoGeneration, webSearch能力,则必须支持文字的输入
|
||||
if (
|
||||
(data.capabilities.includes('toolUse') ||
|
||||
data.capabilities.includes('reasoning') ||
|
||||
data.capabilities.includes('streaming') ||
|
||||
data.capabilities.includes('cache') ||
|
||||
data.capabilities.includes('codeExecution') ||
|
||||
data.capabilities.includes('imageGeneration') ||
|
||||
data.capabilities.includes('audioGeneration') ||
|
||||
data.capabilities.includes('videoGeneration') ||
|
||||
data.capabilities.includes('webSearch')) &&
|
||||
!data.supportedInputs.includes('text')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果模型有toolUse, Reasoning, streaming, cache, codeExecution, OCR, textGeneration, translation, transcription, webSearch, structuredOutput能力,则必须支持文字的输出
|
||||
if (
|
||||
(data.capabilities.includes('toolUse') ||
|
||||
data.capabilities.includes('reasoning') ||
|
||||
data.capabilities.includes('streaming') ||
|
||||
data.capabilities.includes('cache') ||
|
||||
data.capabilities.includes('codeExecution') ||
|
||||
data.capabilities.includes('OCR') ||
|
||||
data.capabilities.includes('textGeneration') ||
|
||||
data.capabilities.includes('translation') ||
|
||||
data.capabilities.includes('transcription') ||
|
||||
data.capabilities.includes('webSearch') ||
|
||||
data.capabilities.includes('structuredOutput')) &&
|
||||
!data.supportedOutputs.includes('text')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
{
|
||||
message: 'ModelCard has inconsistent capabilities and supported input/output type'
|
||||
}
|
||||
)
|
||||
|
||||
export type ModelCard = z.infer<typeof ModelSchema>
|
||||
|
||||
export function createModelCard(model: ModelCard): ModelCard {
|
||||
return ModelSchema.parse(model)
|
||||
}
|
||||
|
||||
export function supportesInputType(model: ModelCard, inputType: InputType) {
|
||||
return model.supportedInputs.includes(inputType)
|
||||
}
|
||||
|
||||
export function supportesOutputType(model: ModelCard, outputType: OutputType) {
|
||||
return model.supportedOutputs.includes(outputType)
|
||||
}
|
||||
|
||||
export function supportesOutputMode(model: ModelCard, outputMode: OutputMode) {
|
||||
return model.supportedOutputModes.includes(outputMode)
|
||||
}
|
||||
|
||||
export function supportesCapability(model: ModelCard, capability: ModelCapability) {
|
||||
return model.capabilities.includes(capability)
|
||||
}
|
||||
|
||||
export function isVisionModel(model: ModelCard) {
|
||||
return supportesInputType(model, 'image')
|
||||
}
|
||||
|
||||
export function isImageGenerationModel(model: ModelCard) {
|
||||
return isVisionModel(model) && supportesCapability(model, 'imageGeneration')
|
||||
}
|
||||
|
||||
export function isAudioModel(model: ModelCard) {
|
||||
return supportesInputType(model, 'audio')
|
||||
}
|
||||
|
||||
export function isAudioGenerationModel(model: ModelCard) {
|
||||
return supportesCapability(model, 'audioGeneration')
|
||||
}
|
||||
|
||||
export function isVideoModel(model: ModelCard) {
|
||||
return supportesInputType(model, 'video')
|
||||
}
|
||||
|
||||
export function isEmbedModel(model: ModelCard) {
|
||||
return supportesOutputType(model, 'vector') && supportesCapability(model, 'embedding')
|
||||
}
|
||||
|
||||
export function isTextEmbeddingModel(model: ModelCard) {
|
||||
return isEmbedModel(model) && supportesInputType(model, 'text') && model.supportedInputs.length === 1
|
||||
}
|
||||
|
||||
export function isMultiModalEmbeddingModel(model: ModelCard) {
|
||||
return isEmbedModel(model) && model.supportedInputs.length > 1
|
||||
}
|
||||
|
||||
export function isRerankModel(model: ModelCard) {
|
||||
return supportesCapability(model, 'rerank')
|
||||
}
|
||||
|
||||
export function isReasoningModel(model: ModelCard) {
|
||||
return supportesCapability(model, 'reasoning')
|
||||
}
|
||||
|
||||
export function isToolUseModel(model: ModelCard) {
|
||||
return supportesCapability(model, 'toolUse')
|
||||
}
|
||||
|
||||
export function isOnlyStreamingModel(model: ModelCard) {
|
||||
return (
|
||||
supportesCapability(model, 'streaming') &&
|
||||
supportesOutputMode(model, 'streaming') &&
|
||||
model.supportedOutputModes.length === 1
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CompletionUsage } from 'openai/resources'
|
||||
import type { CompletionUsage } from 'openai/resources'
|
||||
|
||||
import type {
|
||||
Assistant,
|
||||
|
||||
@@ -15,7 +15,7 @@ import ActionGeneral from './components/ActionGeneral'
|
||||
import ActionTranslate from './components/ActionTranslate'
|
||||
|
||||
const SelectionActionApp: FC = () => {
|
||||
const { language } = useSettings()
|
||||
const { language, customCss } = useSettings()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -61,6 +61,20 @@ const SelectionActionApp: FC = () => {
|
||||
i18n.changeLanguage(language || navigator.language || defaultLanguage)
|
||||
}, [language])
|
||||
|
||||
useEffect(() => {
|
||||
let customCssElement = document.getElementById('user-defined-custom-css') as HTMLStyleElement
|
||||
if (customCssElement) {
|
||||
customCssElement.remove()
|
||||
}
|
||||
|
||||
if (customCss) {
|
||||
customCssElement = document.createElement('style')
|
||||
customCssElement.id = 'user-defined-custom-css'
|
||||
customCssElement.textContent = customCss
|
||||
document.head.appendChild(customCssElement)
|
||||
}
|
||||
}, [customCss])
|
||||
|
||||
useEffect(() => {
|
||||
const contentEl = contentElementRef.current
|
||||
if (contentEl) {
|
||||
|
||||
@@ -65,8 +65,10 @@ const ActionIcons: FC<{
|
||||
|
||||
const renderActionButton = useCallback(
|
||||
(action: ActionItem) => {
|
||||
const displayName = action.isBuiltIn ? t(action.name) : action.name
|
||||
|
||||
return (
|
||||
<ActionButton key={action.id} onClick={() => handleAction(action)}>
|
||||
<ActionButton key={action.id} onClick={() => handleAction(action)} title={isCompact ? displayName : undefined}>
|
||||
<ActionIcon>
|
||||
{action.id === 'copy' ? (
|
||||
renderCopyIcon()
|
||||
@@ -79,9 +81,7 @@ const ActionIcons: FC<{
|
||||
/>
|
||||
)}
|
||||
</ActionIcon>
|
||||
{!isCompact && (
|
||||
<ActionTitle className="btn-title">{action.isBuiltIn ? t(action.name) : action.name}</ActionTitle>
|
||||
)}
|
||||
{!isCompact && <ActionTitle className="btn-title">{displayName}</ActionTitle>}
|
||||
</ActionButton>
|
||||
)
|
||||
},
|
||||
@@ -95,7 +95,7 @@ const ActionIcons: FC<{
|
||||
* demo is used in the settings page
|
||||
*/
|
||||
const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
const { language } = useSettings()
|
||||
const { language, customCss } = useSettings()
|
||||
const { isCompact, actionItems } = useSelectionAssistant()
|
||||
const [animateKey, setAnimateKey] = useState(0)
|
||||
const [copyIconStatus, setCopyIconStatus] = useState<'normal' | 'success' | 'fail'>('normal')
|
||||
@@ -150,6 +150,20 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
|
||||
i18n.changeLanguage(language || navigator.language || defaultLanguage)
|
||||
}, [language])
|
||||
|
||||
useEffect(() => {
|
||||
let customCssElement = document.getElementById('user-defined-custom-css') as HTMLStyleElement
|
||||
if (customCssElement) {
|
||||
customCssElement.remove()
|
||||
}
|
||||
|
||||
if (customCss) {
|
||||
customCssElement = document.createElement('style')
|
||||
customCssElement.id = 'user-defined-custom-css'
|
||||
customCssElement.textContent = customCss
|
||||
document.head.appendChild(customCssElement)
|
||||
}
|
||||
}, [customCss])
|
||||
|
||||
const onHideCleanUp = () => {
|
||||
setCopyIconStatus('normal')
|
||||
setCopyIconAnimation('none')
|
||||
|
||||
Reference in New Issue
Block a user