Merge branch 'main' into feat-knowlege-ocr

This commit is contained in:
eeee0717
2025-06-05 20:43:23 +08:00
33 changed files with 2030 additions and 372 deletions
-85
View File
@@ -1,85 +0,0 @@
diff --git a/core.js b/core.js
index 862d66101f441fb4f47dfc8cff5e2d39e1f5a11e..6464bebbf696c39d35f0368f061ea4236225c162 100644
--- a/core.js
+++ b/core.js
@@ -159,7 +159,7 @@ class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
...this.authHeaders(opts),
};
}
diff --git a/core.mjs b/core.mjs
index 05dbc6cfde51589a2b100d4e4b5b3c1a33b32b89..789fbb4985eb952a0349b779fa83b1a068af6e7e 100644
--- a/core.mjs
+++ b/core.mjs
@@ -152,7 +152,7 @@ export class APIClient {
Accept: 'application/json',
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent(),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
...this.authHeaders(opts),
};
}
diff --git a/error.mjs b/error.mjs
index 7d19f5578040afa004bc887aab1725e8703d2bac..59ec725b6142299a62798ac4bdedb63ba7d9932c 100644
--- a/error.mjs
+++ b/error.mjs
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
- const error = errorResponse?.['error'];
+ const error = errorResponse?.['error'] || errorResponse;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}
diff --git a/resources/embeddings.js b/resources/embeddings.js
index aae578404cb2d09a39ac33fc416f1c215c45eecd..25c54b05bdae64d5c3b36fbb30dc7c8221b14034 100644
--- a/resources/embeddings.js
+++ b/resources/embeddings.js
@@ -36,6 +36,9 @@ class Embeddings extends resource_1.APIResource {
// No encoding_format specified, defaulting to base64 for performance reasons
// See https://github.com/openai/openai-node/pull/1312
let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
+ if (body.model.includes('jina')) {
+ encoding_format = undefined;
+ }
if (hasUserProvidedEncodingFormat) {
Core.debug('Request', 'User defined encoding_format:', body.encoding_format);
}
@@ -47,7 +50,7 @@ class Embeddings extends resource_1.APIResource {
...options,
});
// if the user specified an encoding_format, return the response as-is
- if (hasUserProvidedEncodingFormat) {
+ if (hasUserProvidedEncodingFormat || body.model.includes('jina')) {
return response;
}
// in this stage, we are sure the user did not specify an encoding_format
diff --git a/resources/embeddings.mjs b/resources/embeddings.mjs
index 0df3c6cc79a520e54acb4c2b5f77c43b774035ff..aa488b8a11b2c413c0a663d9a6059d286d7b5faf 100644
--- a/resources/embeddings.mjs
+++ b/resources/embeddings.mjs
@@ -10,6 +10,9 @@ export class Embeddings extends APIResource {
// No encoding_format specified, defaulting to base64 for performance reasons
// See https://github.com/openai/openai-node/pull/1312
let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
+ if (body.model.includes('jina')) {
+ encoding_format = undefined;
+ }
if (hasUserProvidedEncodingFormat) {
Core.debug('Request', 'User defined encoding_format:', body.encoding_format);
}
@@ -21,7 +24,7 @@ export class Embeddings extends APIResource {
...options,
});
// if the user specified an encoding_format, return the response as-is
- if (hasUserProvidedEncodingFormat) {
+ if (hasUserProvidedEncodingFormat || body.model.includes('jina')) {
return response;
}
// in this stage, we are sure the user did not specify an encoding_format
+279
View File
@@ -0,0 +1,279 @@
diff --git a/client.js b/client.js
index 33b4ff6309d5f29187dab4e285d07dac20340bab..8f568637ee9e4677585931fb0284c8165a933f69 100644
--- a/client.js
+++ b/client.js
@@ -433,7 +433,7 @@ class OpenAI {
'User-Agent': this.getUserAgent(),
'X-Stainless-Retry-Count': String(retryCount),
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
- ...(0, detect_platform_1.getPlatformHeaders)(),
+ // ...(0, detect_platform_1.getPlatformHeaders)(),
'OpenAI-Organization': this.organization,
'OpenAI-Project': this.project,
},
diff --git a/client.mjs b/client.mjs
index c34c18213073540ebb296ea540b1d1ad39527906..1ce1a98256d7e90e26ca963582f235b23e996e73 100644
--- a/client.mjs
+++ b/client.mjs
@@ -430,7 +430,7 @@ export class OpenAI {
'User-Agent': this.getUserAgent(),
'X-Stainless-Retry-Count': String(retryCount),
...(options.timeout ? { 'X-Stainless-Timeout': String(Math.trunc(options.timeout / 1000)) } : {}),
- ...getPlatformHeaders(),
+ // ...getPlatformHeaders(),
'OpenAI-Organization': this.organization,
'OpenAI-Project': this.project,
},
diff --git a/core/error.js b/core/error.js
index a12d9d9ccd242050161adeb0f82e1b98d9e78e20..fe3a5462480558bc426deea147f864f12b36f9bd 100644
--- a/core/error.js
+++ b/core/error.js
@@ -40,7 +40,7 @@ class APIError extends OpenAIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: (0, errors_1.castToError)(errorResponse) });
}
- const error = errorResponse?.['error'];
+ const error = errorResponse?.['error'] || errorResponse;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}
diff --git a/core/error.mjs b/core/error.mjs
index 83cefbaffeb8c657536347322d8de9516af479a2..63334b7972ec04882aa4a0800c1ead5982345045 100644
--- a/core/error.mjs
+++ b/core/error.mjs
@@ -36,7 +36,7 @@ export class APIError extends OpenAIError {
if (!status || !headers) {
return new APIConnectionError({ message, cause: castToError(errorResponse) });
}
- const error = errorResponse?.['error'];
+ const error = errorResponse?.['error'] || errorResponse;
if (status === 400) {
return new BadRequestError(status, error, message, headers);
}
diff --git a/resources/embeddings.js b/resources/embeddings.js
index 2404264d4ba0204322548945ebb7eab3bea82173..8f1bc45cc45e0797d50989d96b51147b90ae6790 100644
--- a/resources/embeddings.js
+++ b/resources/embeddings.js
@@ -5,52 +5,64 @@ exports.Embeddings = void 0;
const resource_1 = require("../core/resource.js");
const utils_1 = require("../internal/utils.js");
class Embeddings extends resource_1.APIResource {
- /**
- * Creates an embedding vector representing the input text.
- *
- * @example
- * ```ts
- * const createEmbeddingResponse =
- * await client.embeddings.create({
- * input: 'The quick brown fox jumped over the lazy dog',
- * model: 'text-embedding-3-small',
- * });
- * ```
- */
- create(body, options) {
- const hasUserProvidedEncodingFormat = !!body.encoding_format;
- // No encoding_format specified, defaulting to base64 for performance reasons
- // See https://github.com/openai/openai-node/pull/1312
- let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
- if (hasUserProvidedEncodingFormat) {
- (0, utils_1.loggerFor)(this._client).debug('embeddings/user defined encoding_format:', body.encoding_format);
- }
- const response = this._client.post('/embeddings', {
- body: {
- ...body,
- encoding_format: encoding_format,
- },
- ...options,
- });
- // if the user specified an encoding_format, return the response as-is
- if (hasUserProvidedEncodingFormat) {
- return response;
- }
- // in this stage, we are sure the user did not specify an encoding_format
- // and we defaulted to base64 for performance reasons
- // we are sure then that the response is base64 encoded, let's decode it
- // the returned result will be a float32 array since this is OpenAI API's default encoding
- (0, utils_1.loggerFor)(this._client).debug('embeddings/decoding base64 embeddings from base64');
- return response._thenUnwrap((response) => {
- if (response && response.data) {
- response.data.forEach((embeddingBase64Obj) => {
- const embeddingBase64Str = embeddingBase64Obj.embedding;
- embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(embeddingBase64Str);
- });
- }
- return response;
- });
- }
+ /**
+ * Creates an embedding vector representing the input text.
+ *
+ * @example
+ * ```ts
+ * const createEmbeddingResponse =
+ * await client.embeddings.create({
+ * input: 'The quick brown fox jumped over the lazy dog',
+ * model: 'text-embedding-3-small',
+ * });
+ * ```
+ */
+ create(body, options) {
+ const hasUserProvidedEncodingFormat = !!body.encoding_format;
+ // No encoding_format specified, defaulting to base64 for performance reasons
+ // See https://github.com/openai/openai-node/pull/1312
+ let encoding_format = hasUserProvidedEncodingFormat
+ ? body.encoding_format
+ : "base64";
+ if (body.model.includes("jina")) {
+ encoding_format = undefined;
+ }
+ if (hasUserProvidedEncodingFormat) {
+ (0, utils_1.loggerFor)(this._client).debug(
+ "embeddings/user defined encoding_format:",
+ body.encoding_format
+ );
+ }
+ const response = this._client.post("/embeddings", {
+ body: {
+ ...body,
+ encoding_format: encoding_format,
+ },
+ ...options,
+ });
+ // if the user specified an encoding_format, return the response as-is
+ if (hasUserProvidedEncodingFormat || body.model.includes("jina")) {
+ return response;
+ }
+ // in this stage, we are sure the user did not specify an encoding_format
+ // and we defaulted to base64 for performance reasons
+ // we are sure then that the response is base64 encoded, let's decode it
+ // the returned result will be a float32 array since this is OpenAI API's default encoding
+ (0, utils_1.loggerFor)(this._client).debug(
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
+ if (response && response.data) {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = (0, utils_1.toFloat32Array)(
+ embeddingBase64Str
+ );
+ });
+ }
+ return response;
+ });
+ }
}
exports.Embeddings = Embeddings;
//# sourceMappingURL=embeddings.js.map
diff --git a/resources/embeddings.mjs b/resources/embeddings.mjs
index 19dcaef578c194a89759c4360073cfd4f7dd2cbf..0284e9cc615c900eff508eb595f7360a74bd9200 100644
--- a/resources/embeddings.mjs
+++ b/resources/embeddings.mjs
@@ -2,51 +2,61 @@
import { APIResource } from "../core/resource.mjs";
import { loggerFor, toFloat32Array } from "../internal/utils.mjs";
export class Embeddings extends APIResource {
- /**
- * Creates an embedding vector representing the input text.
- *
- * @example
- * ```ts
- * const createEmbeddingResponse =
- * await client.embeddings.create({
- * input: 'The quick brown fox jumped over the lazy dog',
- * model: 'text-embedding-3-small',
- * });
- * ```
- */
- create(body, options) {
- const hasUserProvidedEncodingFormat = !!body.encoding_format;
- // No encoding_format specified, defaulting to base64 for performance reasons
- // See https://github.com/openai/openai-node/pull/1312
- let encoding_format = hasUserProvidedEncodingFormat ? body.encoding_format : 'base64';
- if (hasUserProvidedEncodingFormat) {
- loggerFor(this._client).debug('embeddings/user defined encoding_format:', body.encoding_format);
- }
- const response = this._client.post('/embeddings', {
- body: {
- ...body,
- encoding_format: encoding_format,
- },
- ...options,
- });
- // if the user specified an encoding_format, return the response as-is
- if (hasUserProvidedEncodingFormat) {
- return response;
- }
- // in this stage, we are sure the user did not specify an encoding_format
- // and we defaulted to base64 for performance reasons
- // we are sure then that the response is base64 encoded, let's decode it
- // the returned result will be a float32 array since this is OpenAI API's default encoding
- loggerFor(this._client).debug('embeddings/decoding base64 embeddings from base64');
- return response._thenUnwrap((response) => {
- if (response && response.data) {
- response.data.forEach((embeddingBase64Obj) => {
- const embeddingBase64Str = embeddingBase64Obj.embedding;
- embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
- });
- }
- return response;
- });
- }
+ /**
+ * Creates an embedding vector representing the input text.
+ *
+ * @example
+ * ```ts
+ * const createEmbeddingResponse =
+ * await client.embeddings.create({
+ * input: 'The quick brown fox jumped over the lazy dog',
+ * model: 'text-embedding-3-small',
+ * });
+ * ```
+ */
+ create(body, options) {
+ const hasUserProvidedEncodingFormat = !!body.encoding_format;
+ // No encoding_format specified, defaulting to base64 for performance reasons
+ // See https://github.com/openai/openai-node/pull/1312
+ let encoding_format = hasUserProvidedEncodingFormat
+ ? body.encoding_format
+ : "base64";
+ if (body.model.includes("jina")) {
+ encoding_format = undefined;
+ }
+ if (hasUserProvidedEncodingFormat) {
+ loggerFor(this._client).debug(
+ "embeddings/user defined encoding_format:",
+ body.encoding_format
+ );
+ }
+ const response = this._client.post("/embeddings", {
+ body: {
+ ...body,
+ encoding_format: encoding_format,
+ },
+ ...options,
+ });
+ // if the user specified an encoding_format, return the response as-is
+ if (hasUserProvidedEncodingFormat || body.model.includes("jina")) {
+ return response;
+ }
+ // in this stage, we are sure the user did not specify an encoding_format
+ // and we defaulted to base64 for performance reasons
+ // we are sure then that the response is base64 encoded, let's decode it
+ // the returned result will be a float32 array since this is OpenAI API's default encoding
+ loggerFor(this._client).debug(
+ "embeddings/decoding base64 embeddings from base64"
+ );
+ return response._thenUnwrap((response) => {
+ if (response && response.data) {
+ response.data.forEach((embeddingBase64Obj) => {
+ const embeddingBase64Str = embeddingBase64Obj.embedding;
+ embeddingBase64Obj.embedding = toFloat32Array(embeddingBase64Str);
+ });
+ }
+ return response;
+ });
+ }
}
//# sourceMappingURL=embeddings.mjs.map
+4 -5
View File
@@ -1,6 +1,6 @@
{
"name": "CherryStudio",
"version": "1.5.0-rc.1",
"version": "1.4.1",
"private": true,
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
@@ -185,7 +185,7 @@
"mime": "^4.0.4",
"motion": "^12.10.5",
"npx-scope-finder": "^1.2.0",
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
"openai": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch",
"p-queue": "^8.1.0",
"playwright": "^1.52.0",
"prettier": "^3.5.3",
@@ -226,11 +226,10 @@
"@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",
"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",
"canvas": "3.1.0",
"openai@npm:^4.77.0": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.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",
"openai@npm:^4.87.3": "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.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"
},
+13 -2
View File
@@ -841,8 +841,9 @@ export class SelectionService {
//ctrlkey pressed
if (this.lastCtrlkeyDownTime === 0) {
this.lastCtrlkeyDownTime = Date.now()
//add the mouse-wheel listener, detect if user is zooming in/out
//add the mouse-wheel&mouse-down listener, detect if user is zooming in/out or multi-selecting
this.selectionHook!.on('mouse-wheel', this.handleMouseWheelCtrlkeyMode)
this.selectionHook!.on('mouse-down', this.handleMouseDownCtrlkeyMode)
return
}
@@ -866,8 +867,9 @@ export class SelectionService {
*/
private handleKeyUpCtrlkeyMode = (data: KeyboardEventData) => {
if (!this.isCtrlkey(data.vkCode)) return
//remove the mouse-wheel listener
//remove the mouse-wheel&mouse-down listener
this.selectionHook!.off('mouse-wheel', this.handleMouseWheelCtrlkeyMode)
this.selectionHook!.off('mouse-down', this.handleMouseDownCtrlkeyMode)
this.lastCtrlkeyDownTime = 0
}
@@ -880,6 +882,15 @@ export class SelectionService {
this.lastCtrlkeyDownTime = -1
}
/**
* Handle mouse down events in ctrlkey trigger mode
* ignore CtrlKey pressing when mouse down is used
* because user is multi-selecting
*/
private handleMouseDownCtrlkeyMode = () => {
this.lastCtrlkeyDownTime = -1
}
//check if the key is ctrl key
private isCtrlkey(vkCode: number) {
return vkCode === 162 || vkCode === 163
+7 -1
View File
@@ -44,6 +44,9 @@
--color-reference-text: #ffffff;
--color-reference-background: #0b0e12;
--color-list-item: #222;
--color-list-item-hover: #1e1e1e;
--modal-background: #1f1f1f;
--color-highlight: rgba(0, 0, 0, 1);
@@ -68,7 +71,7 @@
--chat-background-assistant: #2c2c2c;
--chat-text-user: var(--color-black);
--list-item-border-radius: 16px;
--list-item-border-radius: 20px;
}
[theme-mode='light'] {
@@ -117,6 +120,9 @@
--color-reference-text: #000000;
--color-reference-background: #f1f7ff;
--color-list-item: #eee;
--color-list-item-hover: #f5f5f5;
--modal-background: var(--color-white);
--color-highlight: initial;
+4 -6
View File
@@ -9,6 +9,7 @@ import { useMinapps } from '@renderer/hooks/useMinapps'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { ThemeMode } from '@renderer/types'
import { isEmoji } from '@renderer/utils'
import type { MenuProps } from 'antd'
@@ -19,7 +20,7 @@ import {
Folder,
Languages,
LayoutGrid,
MessageSquareQuote,
MessageSquare,
Moon,
Palette,
Settings,
@@ -35,7 +36,6 @@ import styled from 'styled-components'
import DragableList from '../DragableList'
import MinAppIcon from '../Icons/MinAppIcon'
import UserPopup from '../Popups/UserPopup'
import i18n from '@renderer/i18n'
const Sidebar: FC = () => {
const { hideMinappPopup, openMinapp } = useMinappPopup()
@@ -67,9 +67,7 @@ const Sidebar: FC = () => {
openMinapp({
id: docsId,
name: t('docs.title'),
url: isChinese
? 'https://docs.cherry-ai.com/'
: 'https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us',
url: isChinese ? 'https://docs.cherry-ai.com/' : 'https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us',
logo: AppLogo
})
}
@@ -151,7 +149,7 @@ const MainMenus: FC = () => {
const isRoutes = (path: string): string => (pathname.startsWith(path) && !minappShow ? 'active' : '')
const iconMap = {
assistants: <MessageSquareQuote size={18} className="icon" />,
assistants: <MessageSquare size={18} className="icon" />,
agents: <Sparkle size={18} className="icon" />,
paintings: <Palette size={18} className="icon" />,
translate: <Languages size={18} className="icon" />,
@@ -48,6 +48,10 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
},
ColorPicker: {
fontFamily: 'var(--code-font-family)'
},
Segmented: {
itemActiveBg: 'var(--color-background-mute)',
itemHoverBg: 'var(--color-background-mute)'
}
},
token: {
+4 -1
View File
@@ -10,16 +10,19 @@ export function usePaintings() {
const edit = useAppSelector((state) => state.paintings.edit)
const upscale = useAppSelector((state) => state.paintings.upscale)
const DMXAPIPaintings = useAppSelector((state) => state.paintings.DMXAPIPaintings)
const tokenFluxPaintings = useAppSelector((state) => state.paintings.tokenFluxPaintings)
const dispatch = useAppDispatch()
return {
paintings,
DMXAPIPaintings,
tokenFluxPaintings,
persistentData: {
generate,
remix,
edit,
upscale
upscale,
tokenFluxPaintings
},
addPainting: (namespace: keyof PaintingsState, painting: PaintingAction) => {
dispatch(addPainting({ namespace, painting }))
+11 -1
View File
@@ -960,7 +960,17 @@
"text_desc_required": "Please enter image description first",
"req_error_text": "Operation failed. Please try again. Avoid using 'copyrighted' or 'sensitive' words in your prompt.",
"auto_create_paint": "Auto-create image",
"auto_create_paint_tip": "After the image is generated, a new image will be created automatically."
"auto_create_paint_tip": "After the image is generated, a new image will be created automatically.",
"select_model": "Select Model",
"input_parameters": "Input Parameters",
"input_image": "Input Image",
"generated_image": "Generated Image",
"pricing": "Pricing",
"model_and_pricing": "Model & Pricing",
"per_image": "per image",
"per_images": "per images",
"required_field": "Required field",
"uploaded_input": "Uploaded input"
},
"prompts": {
"explanation": "Explain this concept to me",
+30 -22
View File
@@ -960,7 +960,17 @@
"text_desc_required": "画像の説明を先に入力してください",
"req_error_text": "実行に失敗しました。もう一度お試しください。プロンプトに「著作権用語」や「センシティブな用語」を含めないでください。",
"auto_create_paint": "画像を自動作成",
"auto_create_paint_tip": "画像が生成された後、自動的に新しい画像が作成されます。"
"auto_create_paint_tip": "画像が生成された後、自動的に新しい画像が作成されます。",
"select_model": "モデルを選択",
"input_parameters": "パラメータ入力",
"input_image": "入力画像",
"generated_image": "生成画像",
"pricing": "料金",
"model_and_pricing": "モデルと料金",
"per_image": "1枚あたり",
"per_images": "複数枚あたり",
"required_field": "必須項目",
"uploaded_input": "アップロード済みの入力"
},
"prompts": {
"explanation": "この概念を説明してください",
@@ -1128,26 +1138,6 @@
"markdown_export.show_model_provider.help": "Markdownエクスポート時にモデルプロバイダー(例:OpenAI、Geminiなど)を表示します。",
"minute_interval_one": "{{count}} 分",
"minute_interval_other": "{{count}} 分",
"notion": {
"api_key": "Notion APIキー",
"api_key_placeholder": "Notion APIキーを入力してください",
"check": {
"button": "確認",
"empty_api_key": "Api_keyが設定されていません",
"empty_database_id": "Database_idが設定されていません",
"error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
"fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
"success": "接続に成功しました。"
},
"database_id": "Notion データベースID",
"database_id_placeholder": "Notion データベースIDを入力してください",
"help": "Notion 設定ドキュメント",
"page_name_key": "ページタイトルフィールド名",
"page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
"title": "Notion 設定",
"export_reasoning.title": "エクスポート時に思考チェーンを含める",
"export_reasoning.help": "有効にすると、Notionにエクスポートする際に思考チェーンの内容が含まれます。"
},
"title": "データ設定",
"webdav": {
"autoSync": "自動バックアップ",
@@ -1267,7 +1257,25 @@
"new_folder.button": "新しいフォルダー"
},
"message_title.use_topic_naming.title": "トピック命名モデルを使用してメッセージのタイトルを作成",
"message_title.use_topic_naming.help": "この設定は、すべてのMarkdownエクスポート方法に影響します。"
"message_title.use_topic_naming.help": "この設定は、すべてのMarkdownエクスポート方法に影響します。",
"notion.api_key": "Notion APIキー",
"notion.api_key_placeholder": "Notion APIキーを入力してください",
"notion.check": {
"button": "確認",
"empty_api_key": "Api_keyが設定されていません",
"empty_database_id": "Database_idが設定されていません",
"error": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
"fail": "接続エラー、ネットワーク設定とApi_keyとDatabase_idを確認してください",
"success": "接続に成功しました。"
},
"notion.database_id": "Notion データベースID",
"notion.database_id_placeholder": "Notion データベースIDを入力してください",
"notion.help": "Notion 設定ドキュメント",
"notion.page_name_key": "ページタイトルフィールド名",
"notion.page_name_key_placeholder": "ページタイトルフィールド名を入力してください。デフォルトは Name です",
"notion.title": "Notion 設定",
"notion.export_reasoning.title": "エクスポート時に思考チェーンを含める",
"notion.export_reasoning.help": "有効にすると、Notionにエクスポートする際に思考チェーンの内容が含まれます。"
},
"display.assistant.title": "アシスタント設定",
"display.custom.css": "カスタムCSS",
+11 -1
View File
@@ -960,7 +960,17 @@
"text_desc_required": "Пожалуйста, сначала введите описание изображения",
"req_error_text": "Операция не удалась, повторите попытку. Пожалуйста, избегайте защищенных авторским правом терминов и конфиденциальных слов в запросах.",
"auto_create_paint": "Автоматическое создание изображения",
"auto_create_paint_tip": "После генерации изображения будет автоматически создано новое."
"auto_create_paint_tip": "После генерации изображения будет автоматически создано новое.",
"select_model": "Выбрать модель",
"input_parameters": "Ввести параметры",
"input_image": "Входное изображение",
"generated_image": "Сгенерированное изображение",
"pricing": "Цены",
"model_and_pricing": "Модель и цены",
"per_image": "за изображение",
"per_images": "за изображения",
"required_field": "Обязательное поле",
"uploaded_input": "Загруженный ввод"
},
"prompts": {
"explanation": "Объясните мне этот концепт",
+11 -1
View File
@@ -960,7 +960,17 @@
"text_desc_required": "请先输入图片描述",
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
"auto_create_paint": "自动新建图片",
"auto_create_paint_tip": "在图片生成后,会自动新建图片"
"auto_create_paint_tip": "在图片生成后,会自动新建图片",
"select_model": "选择模型",
"input_parameters": "输入参数",
"input_image": "输入图片",
"generated_image": "生成图片",
"pricing": "定价",
"model_and_pricing": "模型与定价",
"per_image": "每张图片",
"per_images": "每张图片",
"required_field": "必填项",
"uploaded_input": "已上传输入"
},
"prompts": {
"explanation": "帮我解释一下这个概念",
+11 -1
View File
@@ -960,7 +960,17 @@
"text_desc_required": "請先輸入圖片描述",
"req_error_text": "运行失败,请重试。提示词避免“版权词”和”敏感词”哦。",
"auto_create_paint": "自動新增圖片",
"auto_create_paint_tip": "圖片生成後,會自動新增圖片"
"auto_create_paint_tip": "圖片生成後,會自動新增圖片",
"select_model": "選擇模型",
"input_parameters": "輸入參數",
"input_image": "輸入圖片",
"generated_image": "生成圖片",
"pricing": "定價",
"model_and_pricing": "模型與定價",
"per_image": "每張圖片",
"per_images": "每張圖片",
"required_field": "必填欄位",
"uploaded_input": "已上傳輸入"
},
"prompts": {
"explanation": "幫我解釋一下這個概念",
@@ -34,7 +34,7 @@ const Container = styled.div<{ $isDark: boolean }>`
margin: 5px 20px 0 20px;
border-radius: 10px;
cursor: pointer;
border: 1px solid var(--color-border);
border: 0.5px solid var(--color-border);
`
const Text = styled.div`
@@ -58,12 +58,14 @@ const Assistants: FC<AssistantsTabProps> = ({
<div style={{ marginBottom: '8px' }}>
{getGroupedAssistants.map((group) => (
<TagsContainer key={group.tag}>
<GroupTitle>
<Tooltip title={group.tag}>
<GroupTitleName>{group.tag}</GroupTitleName>
</Tooltip>
<Divider style={{ margin: '12px 0' }}></Divider>
</GroupTitle>
{group.tag !== t('assistants.tags.untagged') && (
<GroupTitle>
<Tooltip title={group.tag}>
<GroupTitleName>{group.tag}</GroupTitleName>
</Tooltip>
<Divider style={{ margin: '12px 0' }}></Divider>
</GroupTitle>
)}
{group.assistants.map((assistant) => (
<AssistantItem
key={assistant.id}
@@ -60,7 +60,7 @@ import {
} from '@renderer/types'
import { modalConfirm } from '@renderer/utils'
import { Button, Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
import { CircleHelp, RotateCcw, Settings2 } from 'lucide-react'
import { CircleHelp, Settings2 } from 'lucide-react'
import { FC, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -72,7 +72,7 @@ interface Props {
}
const SettingsTab: FC<Props> = (props) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const { assistant, updateAssistantSettings } = useAssistant(props.assistant.id)
const { provider } = useProvider(assistant.model.provider)
const { messageStyle, fontSize, language } = useSettings()
@@ -140,24 +140,6 @@ const SettingsTab: FC<Props> = (props) => {
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setContextCount(DEFAULT_CONTEXTCOUNT)
updateAssistant({
...assistant,
settings: {
...assistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONTEXTCOUNT,
enableMaxTokens: false,
maxTokens: DEFAULT_MAX_TOKENS,
streamOutput: true,
hideMessages: false,
customParameters: []
}
})
}
const codeStyle = useMemo(() => {
return codeEditor.enabled
? theme === ThemeMode.light
@@ -211,14 +193,6 @@ const SettingsTab: FC<Props> = (props) => {
defaultExpanded={true}
extra={
<HStack alignItems="center" gap={2}>
<Tooltip title={t('chat.settings.reset')}>
<Button
type="text"
size="small"
onClick={onReset}
icon={<RotateCcw size={20} style={{ cursor: 'pointer', padding: '0 3px', opacity: 0.8 }} />}
/>
</Tooltip>
<Button
type="text"
size="small"
@@ -714,6 +688,7 @@ const Container = styled(Scrollbar)`
padding-right: 0;
padding-top: 2px;
padding-bottom: 10px;
margin-top: 3px;
`
const SettingRowTitleSmall = styled(SettingRowTitle)`
+36 -23
View File
@@ -4,6 +4,7 @@ import {
DeleteOutlined,
EditOutlined,
FolderOutlined,
MenuOutlined,
PushpinOutlined,
QuestionCircleOutlined,
UploadOutlined
@@ -54,7 +55,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
const { assistants } = useAssistants()
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation()
const { showTopicTime, pinTopicsToTop } = useSettings()
const { showTopicTime, pinTopicsToTop, setTopicPosition } = useSettings()
const borderRadius = showTopicTime ? 12 : 'var(--list-item-border-radius)'
@@ -248,6 +249,23 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
})
}
},
{
label: t('settings.topic.position'),
key: 'topic-position',
icon: <MenuOutlined />,
children: [
{
label: t('settings.topic.position.left'),
key: 'left',
onClick: () => setTopicPosition('left')
},
{
label: t('settings.topic.position.right'),
key: 'right',
onClick: () => setTopicPosition('right')
}
]
},
{
label: t('chat.topics.copy.title'),
key: 'copy',
@@ -363,26 +381,27 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
return menus
}, [
activeTopic.id,
assistant,
assistants,
exportMenuOptions.docx,
targetTopic,
t,
exportMenuOptions.image,
exportMenuOptions.joplin,
exportMenuOptions.markdown,
exportMenuOptions.markdown_reason,
exportMenuOptions.docx,
exportMenuOptions.notion,
exportMenuOptions.obsidian,
exportMenuOptions.siyuan,
exportMenuOptions.yuque,
onClearMessages,
onDeleteTopic,
onMoveTopic,
onPinTopic,
setActiveTopic,
t,
exportMenuOptions.obsidian,
exportMenuOptions.joplin,
exportMenuOptions.siyuan,
assistants,
assistant,
updateTopic,
targetTopic
activeTopic.id,
setActiveTopic,
onPinTopic,
onClearMessages,
setTopicPosition,
onMoveTopic,
onDeleteTopic
])
// Sort topics based on pinned status if pinTopicsToTop is enabled
@@ -486,7 +505,6 @@ const TopicListItem = styled.div`
justify-content: space-between;
position: relative;
cursor: pointer;
border: 0.5px solid transparent;
position: relative;
width: calc(var(--assistants-width) - 20px);
.menu {
@@ -494,15 +512,10 @@ const TopicListItem = styled.div`
color: var(--color-text-3);
}
&:hover {
background-color: var(--color-background-soft);
.name {
}
background-color: var(--color-list-item-hover);
}
&.active {
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
.name {
}
background-color: var(--color-list-item);
.menu {
opacity: 1;
&:hover {
@@ -1,4 +1,5 @@
import {
CheckOutlined,
DeleteOutlined,
EditOutlined,
MinusCircleOutlined,
@@ -185,10 +186,9 @@ const handleTagOperation = (
updateAssistants: (assistants: Assistant[]) => void
) => {
if (assistant.tags?.includes(tag)) {
updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [] } : a)))
} else {
updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [tag] } : a)))
return
}
updateAssistants(assistants.map((a) => (a.id === assistant.id ? { ...a, tags: [tag] } : a)))
}
// 提取创建菜单项的函数
@@ -202,8 +202,7 @@ const createTagMenuItems = (
const items: MenuProps['items'] = [
...allTags.map((tag) => ({
label: tag,
icon: assistant.tags?.includes(tag) ? <DeleteOutlined size={14} /> : <Tag size={12} />,
danger: assistant.tags?.includes(tag),
icon: assistant.tags?.includes(tag) ? <CheckOutlined size={14} /> : <Tag size={12} />,
key: `all-tag-${tag}`,
onClick: () => handleTagOperation(tag, assistant, assistants, updateAssistants)
}))
@@ -383,23 +382,18 @@ const Container = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 10px;
padding: 0 8px;
height: 37px;
position: relative;
border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px);
cursor: pointer;
.iconfont {
opacity: 0;
color: var(--color-text-3);
}
&:hover {
background-color: var(--color-background-soft);
background-color: var(--color-list-item-hover);
}
&.active {
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
background-color: var(--color-list-item);
}
`
+47 -40
View File
@@ -58,6 +58,7 @@ const HomeTabs: FC<Props> = ({
const assistantTab = {
label: t('assistants.abbr'),
value: 'assistants'
// icon: <BotIcon size={16} />
}
const onCreateAssistant = async () => {
@@ -104,28 +105,35 @@ const HomeTabs: FC<Props> = ({
return (
<Container style={{ ...border, ...style }} className="home-tabs">
{(showTab || (forceToSeeAllTab == true && !showTopics)) && (
<Segmented
value={tab}
style={{ borderRadius: 16, paddingTop: 10, margin: '0 10px', gap: 2 }}
options={
[
(position === 'left' && topicPosition === 'left') || (forceToSeeAllTab == true && position === 'left')
? assistantTab
: undefined,
{
label: t('common.topics'),
value: 'topic'
},
{
label: t('settings.title'),
value: 'settings'
}
].filter(Boolean) as SegmentedProps['options']
}
onChange={(value) => setTab(value as 'topic' | 'settings')}
block
/>
<>
<Segmented
value={tab}
style={{ borderRadius: 50 }}
shape="round"
options={
[
(position === 'left' && topicPosition === 'left') || (forceToSeeAllTab == true && position === 'left')
? assistantTab
: undefined,
{
label: t('common.topics'),
value: 'topic'
// icon: <MessageSquareQuote size={16} />
},
{
label: t('settings.title'),
value: 'settings'
// icon: <SettingsIcon size={16} />
}
].filter(Boolean) as SegmentedProps['options']
}
onChange={(value) => setTab(value as 'topic' | 'settings')}
block
/>
<Divider />
</>
)}
<TabContent className="home-tabs-content">
{tab === 'assistants' && (
<Assistants
@@ -149,7 +157,7 @@ const Container = styled.div`
flex-direction: column;
max-width: var(--assistants-width);
min-width: var(--assistants-width);
background-color: var(--color-background);
background-color: transparent;
overflow: hidden;
.collapsed {
width: 0;
@@ -165,14 +173,21 @@ const TabContent = styled.div`
overflow-x: hidden;
`
const Divider = styled.div`
border-top: 0.5px solid var(--color-border);
margin-top: 10px;
margin-left: 10px;
margin-right: 10px;
`
const Segmented = styled(AntSegmented)`
font-family: var(--font-family);
&.ant-segmented {
background-color: transparent;
border-radius: 0 !important;
border-bottom: 0.5px solid var(--color-border);
padding-bottom: 10px;
margin: 0 10px;
margin-top: 10px;
padding: 0;
}
.ant-segmented-item {
overflow: hidden;
@@ -184,10 +199,10 @@ const Segmented = styled(AntSegmented)`
border-radius: var(--list-item-border-radius);
box-shadow: none;
}
.ant-segmented-item-selected {
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
.ant-segmented-item-selected,
.ant-segmented-item-selected:active {
transition: none !important;
background-color: var(--color-list-item);
}
.ant-segmented-item-label {
align-items: center;
@@ -200,25 +215,17 @@ const Segmented = styled(AntSegmented)`
.ant-segmented-item-label[aria-selected='true'] {
color: var(--color-text);
}
.iconfont {
font-size: 13px;
margin-left: -2px;
}
.anticon-setting {
font-size: 12px;
}
.icon-business-smart-assistant {
margin-right: -2px;
}
.ant-segmented-item-icon + * {
margin-left: 4px;
}
.ant-segmented-thumb {
transition: none !important;
background-color: var(--color-background-soft);
border: 0.5px solid var(--color-border);
background-color: var(--color-list-item);
border-radius: var(--list-item-border-radius);
box-shadow: none;
&:hover {
background-color: transparent;
}
}
.ant-segmented-item-label,
.ant-segmented-item-icon {
@@ -7,8 +7,9 @@ import { Route, Routes, useParams } from 'react-router-dom'
import AihubmixPage from './AihubmixPage'
import DmxapiPage from './DmxapiPage'
import SiliconPage from './SiliconPage'
import TokenFluxPage from './TokenFluxPage'
const Options = ['aihubmix', 'silicon', 'dmxapi']
const Options = ['aihubmix', 'silicon', 'dmxapi', 'tokenflux']
const PaintingsRoutePage: FC = () => {
const params = useParams()
@@ -28,6 +29,7 @@ const PaintingsRoutePage: FC = () => {
<Route path="/aihubmix" element={<AihubmixPage Options={Options} />} />
<Route path="/silicon" element={<SiliconPage Options={Options} />} />
<Route path="/dmxapi" element={<DmxapiPage Options={Options} />} />
<Route path="/tokenflux" element={<TokenFluxPage Options={Options} />} />
</Routes>
)
}
@@ -0,0 +1,786 @@
import { PlusOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navbar'
import Scrollbar from '@renderer/components/Scrollbar'
import TranslateButton from '@renderer/components/TranslateButton'
import { isMac } from '@renderer/config/constant'
import { getProviderLogo } from '@renderer/config/providers'
import { usePaintings } from '@renderer/hooks/usePaintings'
import { useAllProviders } from '@renderer/hooks/useProvider'
import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import FileManager from '@renderer/services/FileManager'
import { translateText } from '@renderer/services/TranslateService'
import { useAppDispatch } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import type { TokenFluxPainting } from '@renderer/types'
import { getErrorMessage, uuid } from '@renderer/utils'
import { Avatar, Button, Select, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { Info } from 'lucide-react'
import type { FC } 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 './components/Artboard'
import { DynamicFormRender } from './components/DynamicFormRender'
import PaintingsList from './components/PaintingsList'
import { DEFAULT_TOKENFLUX_PAINTING, type TokenFluxModel } from './config/tokenFluxConfig'
import TokenFluxService from './utils/TokenFluxService'
const TokenFluxPage: FC<{ Options: string[] }> = ({ Options }) => {
const [models, setModels] = useState<TokenFluxModel[]>([])
const [selectedModel, setSelectedModel] = useState<TokenFluxModel | null>(null)
const [formData, setFormData] = useState<Record<string, any>>({})
const [currentImageIndex, setCurrentImageIndex] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [abortController, setAbortController] = useState<AbortController | null>(null)
const [spaceClickCount, setSpaceClickCount] = useState(0)
const [isTranslating, setIsTranslating] = useState(false)
const { t, i18n } = useTranslation()
const providers = useAllProviders()
const { addPainting, removePainting, updatePainting, persistentData } = usePaintings()
const tokenFluxPaintings = useMemo(() => persistentData.tokenFluxPaintings || [], [persistentData.tokenFluxPaintings])
const [painting, setPainting] = useState<TokenFluxPainting>(
tokenFluxPaintings[0] || { ...DEFAULT_TOKENFLUX_PAINTING, id: uuid() }
)
const providerOptions = Options.map((option) => {
const provider = providers.find((p) => p.id === option)
return {
label: t(`provider.${provider?.id}`),
value: provider?.id
}
})
const dispatch = useAppDispatch()
const { generating } = useRuntime()
const navigate = useNavigate()
const location = useLocation()
const { autoTranslateWithSpace } = useSettings()
const spaceClickTimer = useRef<NodeJS.Timeout>(null)
const tokenfluxProvider = providers.find((p) => p.id === 'tokenflux')!
const textareaRef = useRef<any>(null)
const tokenFluxService = useMemo(
() => new TokenFluxService(tokenfluxProvider.apiHost, tokenfluxProvider.apiKey),
[tokenfluxProvider]
)
useEffect(() => {
tokenFluxService.fetchModels().then((models) => {
setModels(models)
if (models.length > 0) {
setSelectedModel(models[0])
}
})
}, [tokenFluxService])
const getNewPainting = useCallback(() => {
return {
...DEFAULT_TOKENFLUX_PAINTING,
id: uuid(),
model: selectedModel?.id || '',
inputParams: {},
generationId: undefined
}
}, [selectedModel])
const updatePaintingState = useCallback(
(updates: Partial<TokenFluxPainting>) => {
setPainting((prevPainting) => {
const updatedPainting = { ...prevPainting, ...updates }
updatePainting('tokenFluxPaintings', updatedPainting)
return updatedPainting
})
},
[updatePainting]
)
const handleError = (error: unknown) => {
if (error instanceof Error && error.name !== 'AbortError') {
window.modal.error({
content: getErrorMessage(error),
centered: true
})
}
}
const handleModelChange = (modelId: string) => {
const model = models.find((m) => m.id === modelId)
if (model) {
setSelectedModel(model)
setFormData({})
updatePaintingState({ model: model.id, inputParams: {} })
}
}
const handleFormFieldChange = (field: string, value: any) => {
const newFormData = { ...formData, [field]: value }
setFormData(newFormData)
updatePaintingState({ inputParams: newFormData })
}
const onGenerate = async () => {
if (painting.files.length > 0) {
const confirmed = await window.modal.confirm({
content: t('paintings.regenerate.confirm'),
centered: true
})
if (!confirmed) return
await FileManager.deleteFiles(painting.files)
}
const prompt = textareaRef.current?.resizableTextArea?.textArea?.value || ''
if (!tokenfluxProvider.enabled) {
window.modal.error({
content: t('error.provider_disabled'),
centered: true
})
return
}
if (!tokenfluxProvider.apiKey) {
window.modal.error({
content: t('error.no_api_key'),
centered: true
})
return
}
if (!selectedModel || !prompt) {
window.modal.error({
content: t('paintings.text_desc_required'),
centered: true
})
return
}
const controller = new AbortController()
setAbortController(controller)
setIsLoading(true)
dispatch(setGenerating(true))
try {
const requestBody = {
model: selectedModel.id,
input: {
prompt,
...formData
}
}
const inputParams = { prompt, ...formData }
updatePaintingState({
model: selectedModel.id,
prompt,
status: 'processing',
inputParams
})
const result = await tokenFluxService.generateAndWait(requestBody, {
signal: controller.signal,
onStatusUpdate: (updates) => {
updatePaintingState(updates)
}
})
if (result && result.images && result.images.length > 0) {
const urls = result.images.map((img: { url: string }) => img.url)
const validFiles = await tokenFluxService.downloadImages(urls)
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls, status: 'succeeded' })
}
setIsLoading(false)
dispatch(setGenerating(false))
setAbortController(null)
} catch (error: unknown) {
handleError(error)
setIsLoading(false)
dispatch(setGenerating(false))
setAbortController(null)
}
}
const onCancel = () => {
abortController?.abort()
setIsLoading(false)
dispatch(setGenerating(false))
setAbortController(null)
}
const nextImage = () => {
setCurrentImageIndex((prev) => (prev + 1) % painting.files.length)
}
const prevImage = () => {
setCurrentImageIndex((prev) => (prev - 1 + painting.files.length) % painting.files.length)
}
const handleAddPainting = () => {
const newPainting = addPainting('tokenFluxPaintings', getNewPainting())
updatePainting('tokenFluxPaintings', newPainting)
setPainting(newPainting as TokenFluxPainting)
return newPainting
}
const onDeletePainting = (paintingToDelete: TokenFluxPainting) => {
if (paintingToDelete.id === painting.id) {
const currentIndex = tokenFluxPaintings.findIndex((p) => p.id === paintingToDelete.id)
if (currentIndex > 0) {
setPainting(tokenFluxPaintings[currentIndex - 1])
} else if (tokenFluxPaintings.length > 1) {
setPainting(tokenFluxPaintings[1])
}
}
removePainting('tokenFluxPaintings', paintingToDelete)
}
const translate = async () => {
if (isTranslating) {
return
}
if (!painting.prompt) {
return
}
try {
setIsTranslating(true)
const translatedText = await translateText(painting.prompt, 'english')
updatePaintingState({ prompt: translatedText })
} catch (error) {
console.error('Translation failed:', error)
} finally {
setIsTranslating(false)
}
}
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (autoTranslateWithSpace && event.key === ' ') {
setSpaceClickCount((prev) => prev + 1)
if (spaceClickTimer.current) {
clearTimeout(spaceClickTimer.current)
}
spaceClickTimer.current = setTimeout(() => {
setSpaceClickCount(0)
}, 200)
if (spaceClickCount === 2) {
setSpaceClickCount(0)
setIsTranslating(true)
translate()
}
}
}
const handleProviderChange = (providerId: string) => {
const routeName = location.pathname.split('/').pop()
if (providerId !== routeName) {
navigate('../' + providerId, { replace: true })
}
}
const onSelectPainting = (newPainting: TokenFluxPainting) => {
if (generating) return
setPainting(newPainting)
setCurrentImageIndex(0)
// Set form data from painting's input params
if (newPainting.inputParams) {
// Filter out the prompt from inputParams since it's handled separately
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { prompt, ...formInputParams } = newPainting.inputParams
setFormData(formInputParams)
} else {
setFormData({})
}
// Set selected model if available
if (newPainting.model) {
const model = models.find((m) => m.id === newPainting.model)
if (model) {
setSelectedModel(model)
}
} else {
setSelectedModel(null)
}
}
const readI18nContext = (property: Record<string, any>, key: string): string => {
const lang = i18n.language.split('-')[0] // Get the base language code (e.g., 'en' from 'en-US')
console.log('readI18nContext', { property, key, lang })
return property[`${key}_${lang}`] || property[key]
}
useEffect(() => {
if (tokenFluxPaintings.length === 0) {
const newPainting = getNewPainting()
addPainting('tokenFluxPaintings', newPainting)
setPainting(newPainting)
}
}, [tokenFluxPaintings, addPainting, getNewPainting])
useEffect(() => {
const timer = spaceClickTimer.current
return () => {
if (timer) {
clearTimeout(timer)
}
}
}, [])
useEffect(() => {
if (painting.status === 'processing' && painting.generationId) {
tokenFluxService
.pollGenerationResult(painting.generationId, {
onStatusUpdate: (updates) => {
console.log('Polling status update:', updates)
updatePaintingState(updates)
}
})
.then((result) => {
if (result && result.images && result.images.length > 0) {
const urls = result.images.map((img: { url: string }) => img.url)
tokenFluxService.downloadImages(urls).then(async (validFiles) => {
await FileManager.addFiles(validFiles)
updatePaintingState({ files: validFiles, urls, status: 'succeeded' })
})
}
})
.catch((error) => {
console.error('Polling failed:', error)
updatePaintingState({ status: 'failed' })
})
}
}, [painting.generationId, painting.status, tokenFluxService, updatePaintingState])
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('paintings.title')}</NavbarCenter>
{isMac && (
<NavbarRight style={{ justifyContent: 'flex-end' }}>
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={handleAddPainting}>
{t('paintings.button.new.image')}
</Button>
</NavbarRight>
)}
</Navbar>
<ContentContainer id="content-container">
<LeftContainer>
{/* Provider Section */}
<ProviderTitleContainer>
<SettingTitle style={{ marginBottom: 8 }}>{t('common.provider')}</SettingTitle>
<SettingHelpLink target="_blank" href="https://tokenflux.ai">
{t('paintings.learn_more')}
<ProviderLogo shape="square" src={getProviderLogo('tokenflux')} size={16} style={{ marginLeft: 5 }} />
</SettingHelpLink>
</ProviderTitleContainer>
<Select
value={providerOptions.find((p) => p.value === 'tokenflux')?.value}
onChange={handleProviderChange}
style={{ width: '100%' }}>
{providerOptions.map((provider) => (
<Select.Option value={provider.value} key={provider.value}>
<SelectOptionContainer>
<ProviderLogo shape="square" src={getProviderLogo(provider.value || '')} size={16} />
{provider.label}
</SelectOptionContainer>
</Select.Option>
))}
</Select>
{/* Model & Pricing Section */}
<SectionTitle
style={{ marginBottom: 5, marginTop: 15, justifyContent: 'space-between', alignItems: 'center' }}>
{t('paintings.model_and_pricing')}
{selectedModel && selectedModel.pricing && (
<PricingContainer>
<PricingBadge>
{selectedModel.pricing.price} {selectedModel.pricing.currency}{' '}
{selectedModel.pricing.unit > 1 ? t('paintings.per_images') : t('paintings.per_image')}
</PricingBadge>
</PricingContainer>
)}
</SectionTitle>
<Select
style={{ width: '100%', marginBottom: 12 }}
value={selectedModel?.id}
onChange={handleModelChange}
placeholder={t('paintings.select_model')}>
{Object.entries(
models.reduce(
(acc, model) => {
const provider = model.model_provider || 'Other'
if (!acc[provider]) {
acc[provider] = []
}
acc[provider].push(model)
return acc
},
{} as Record<string, typeof models>
)
).map(([provider, providerModels]) => (
<Select.OptGroup key={provider} label={provider}>
{providerModels.map((model) => (
<Select.Option key={model.id} value={model.id}>
<Tooltip title={model.description} placement="right">
<ModelOptionContainer>
<ModelName>{model.name}</ModelName>
</ModelOptionContainer>
</Tooltip>
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
{/* Input Parameters Section */}
{selectedModel && selectedModel.input_schema && (
<>
<SectionTitle style={{ marginBottom: 5, marginTop: 10 }}>{t('paintings.input_parameters')}</SectionTitle>
<ParametersContainer>
{Object.entries(selectedModel.input_schema.properties).map(([key, property]: [string, any]) => {
if (key === 'prompt') return null // Skip prompt as it's handled separately
const isRequired = selectedModel.input_schema.required?.includes(key)
return (
<ParameterField key={key}>
<ParameterLabel>
<ParameterName>
{readI18nContext(property, 'title')}
{isRequired && <RequiredIndicator> *</RequiredIndicator>}
</ParameterName>
{property.description && (
<Tooltip title={readI18nContext(property, 'description')}>
<InfoIcon />
</Tooltip>
)}
</ParameterLabel>
<DynamicFormRender
schemaProperty={property}
propertyName={key}
value={formData[key]}
onChange={handleFormFieldChange}
/>
</ParameterField>
)
})}
</ParametersContainer>
</>
)}
</LeftContainer>
<MainContainer>
{/* Check if any form field contains an uploaded image */}
{Object.keys(formData).some((key) => key.toLowerCase().includes('image') && formData[key]) ? (
<ComparisonContainer>
<ImageComparisonSection>
<SectionLabel>{t('paintings.input_image')}</SectionLabel>
<UploadedImageContainer>
{Object.entries(formData).map(([key, value]) => {
if (key.toLowerCase().includes('image') && value) {
return (
<ImageWrapper key={key}>
<img
src={value}
alt={t('paintings.uploaded_input')}
style={{
maxWidth: '100%',
maxHeight: '70vh',
objectFit: 'contain',
backgroundColor: 'var(--color-background-soft)'
}}
/>
</ImageWrapper>
)
}
return null
})}
</UploadedImageContainer>
</ImageComparisonSection>
<ImageComparisonSection>
<SectionLabel>{t('paintings.generated_image')}</SectionLabel>
<Artboard
painting={painting}
isLoading={isLoading}
currentImageIndex={currentImageIndex}
onPrevImage={prevImage}
onNextImage={nextImage}
onCancel={onCancel}
/>
</ImageComparisonSection>
</ComparisonContainer>
) : (
<Artboard
painting={painting}
isLoading={isLoading}
currentImageIndex={currentImageIndex}
onPrevImage={prevImage}
onNextImage={nextImage}
onCancel={onCancel}
/>
)}
<InputContainer>
<Textarea
ref={textareaRef}
variant="borderless"
disabled={isLoading}
value={painting.prompt || ''}
spellCheck={false}
onChange={(e) => updatePaintingState({ prompt: e.target.value })}
placeholder={isTranslating ? t('paintings.translating') : t('paintings.prompt_placeholder')}
onKeyDown={handleKeyDown}
/>
<Toolbar>
<ToolbarMenu>
<TranslateButton
text={textareaRef.current?.resizableTextArea?.textArea?.value}
onTranslated={(translatedText) => updatePaintingState({ prompt: translatedText })}
disabled={isLoading || isTranslating}
isLoading={isTranslating}
style={{ marginRight: 6, borderRadius: '50%' }}
/>
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
</ToolbarMenu>
</Toolbar>
</InputContainer>
</MainContainer>
<PaintingsList
namespace="tokenFluxPaintings"
paintings={tokenFluxPaintings}
selectedPainting={painting}
onSelectPainting={onSelectPainting as any}
onDeletePainting={onDeletePainting as any}
onNewPainting={handleAddPainting}
/>
</ContentContainer>
</Container>
)
}
const SectionTitle = styled.div`
font-size: 14px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 12px;
display: flex;
align-items: center;
`
const ModelOptionContainer = styled.div`
display: flex;
flex-direction: column;
`
const ModelName = styled.div`
color: var(--color-text);
`
const PricingContainer = styled.div`
display: flex;
justify-content: flex-end;
`
const PricingBadge = styled.div`
background-color: var(--color-primary-bg);
color: var(--color-primary);
font-size: 11px;
font-weight: 500;
padding: 4px 0;
border-radius: 4px;
border: 1px solid var(--color-primary-border);
`
const ParametersContainer = styled.div`
display: flex;
flex-direction: column;
gap: 12px;
`
const ParameterField = styled.div`
display: flex;
flex-direction: column;
`
const ParameterLabel = styled.div`
display: flex;
align-items: center;
margin-bottom: 6px;
`
const ParameterName = styled.span`
font-size: 13px;
font-weight: 500;
color: var(--color-text);
text-transform: capitalize;
`
const RequiredIndicator = styled.span`
color: var(--color-error);
font-weight: 600;
`
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
height: 100%;
background-color: var(--color-background);
overflow: hidden;
`
const LeftContainer = styled(Scrollbar)`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding: 20px;
background-color: var(--color-background);
max-width: var(--assistants-width);
border-right: 0.5px solid var(--color-border);
`
const MainContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
background-color: var(--color-background);
`
const ComparisonContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
height: 100%;
gap: 1px;
`
const ImageComparisonSection = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
background-color: var(--color-background);
&:first-child {
border-right: 0.5px solid var(--color-border);
}
`
const SectionLabel = styled.div`
padding: 10px 20px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-2);
background-color: var(--color-background-soft);
border-bottom: 1px solid var(--color-border);
text-align: center;
`
const UploadedImageContainer = styled.div`
display: flex;
flex: 1;
justify-content: center;
align-items: center;
background-color: var(--color-background);
`
const ImageWrapper = styled.div`
position: relative;
display: flex;
justify-content: center;
align-items: center;
`
const InputContainer = styled.div`
display: flex;
flex-direction: column;
min-height: 95px;
max-height: 95px;
position: relative;
border: 1px solid var(--color-border-soft);
transition: all 0.3s ease;
margin: 0 20px 15px 20px;
border-radius: 10px;
`
const Textarea = styled(TextArea)`
padding: 10px;
border-radius: 0;
display: flex;
flex: 1;
resize: none !important;
overflow: auto;
width: auto;
`
const Toolbar = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
justify-content: flex-end;
padding: 0 8px;
padding-bottom: 0;
height: 40px;
`
const ToolbarMenu = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
`
const InfoIcon = styled(Info)`
margin-left: 5px;
cursor: help;
color: var(--color-text-2);
opacity: 0.6;
width: 14px;
height: 16px;
&:hover {
opacity: 1;
}
`
const ProviderLogo = styled(Avatar)`
border: 0.5px solid var(--color-border);
`
const ProviderTitleContainer = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
`
const SelectOptionContainer = styled.div`
display: flex;
align-items: center;
gap: 8px;
`
export default TokenFluxPage
@@ -0,0 +1,213 @@
import { CloseOutlined, LinkOutlined, RedoOutlined, UploadOutlined } from '@ant-design/icons'
import { convertToBase64 } from '@renderer/utils'
import { Button, Input, InputNumber, Select, Switch, Upload } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useCallback } from 'react'
interface DynamicFormRenderProps {
schemaProperty: any
propertyName: string
value: any
onChange: (field: string, value: any) => void
}
export const DynamicFormRender: React.FC<DynamicFormRenderProps> = ({
schemaProperty,
propertyName,
value,
onChange
}) => {
const { type, enum: enumValues, description, default: defaultValue, format } = schemaProperty
const handleImageUpload = useCallback(
async (
propertyName: string,
fileOrUrl: File | string,
onChange: (field: string, value: any) => void
): Promise<void> => {
try {
if (typeof fileOrUrl === 'string') {
// Handle URL case - validate and set directly
if (fileOrUrl.match(/^https?:\/\/.+\.(jpg|jpeg|png|gif|webp|svg)(\?.*)?$/i)) {
onChange(propertyName, fileOrUrl)
} else {
window.message?.error('Invalid image URL format')
}
} else {
// Handle File case - convert to base64
const base64Image = await convertToBase64(fileOrUrl)
if (typeof base64Image === 'string') {
onChange(propertyName, base64Image)
} else {
console.error('Failed to convert image to base64')
}
}
} catch (error) {
console.error('Error processing image:', error)
}
},
[]
)
if (type === 'string' && propertyName.toLowerCase().includes('image') && format === 'uri') {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', gap: '0' }}>
<Input
style={{
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
borderRight: 'none'
}}
value={value || defaultValue || ''}
onChange={(e) => onChange(propertyName, e.target.value)}
placeholder="Enter image URL or upload file"
prefix={<LinkOutlined style={{ color: '#999' }} />}
/>
<Upload
accept="image/*"
showUploadList={false}
beforeUpload={(file) => {
handleImageUpload(propertyName, file, onChange)
return false
}}>
<Button
icon={<UploadOutlined />}
title="Upload image file"
style={{
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
height: '32px'
}}
/>
</Upload>
</div>
{value && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px',
backgroundColor: 'var(--color-fill-quaternary)',
borderRadius: '6px',
border: '1px solid var(--color-border)'
}}>
<img
src={value}
alt="Image preview"
style={{
width: '48px',
height: '48px',
objectFit: 'cover',
borderRadius: '4px',
border: '1px solid var(--color-border-secondary)',
boxShadow: '0 1px 4px rgba(0, 0, 0, 0.1)',
flexShrink: 0
}}
/>
<div
style={{
flex: 1,
fontSize: '12px',
color: 'var(--color-text-secondary)',
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis'
}}>
{value.startsWith('data:') ? 'Uploaded image' : 'Image URL'}
</div>
<Button
size="small"
danger
icon={<CloseOutlined />}
onClick={() => onChange(propertyName, '')}
title="Remove image"
style={{ flexShrink: 0, minWidth: 'auto', padding: '0 8px' }}
/>
</div>
)}
</div>
)
}
if (type === 'string' && enumValues) {
return (
<Select
style={{ width: '100%' }}
value={value || defaultValue}
options={enumValues.map((val: string) => ({ label: val, value: val }))}
onChange={(v) => onChange(propertyName, v)}
/>
)
}
if (type === 'string') {
if (propertyName.toLowerCase().includes('prompt') && propertyName !== 'prompt') {
return (
<TextArea
value={value || defaultValue || ''}
onChange={(e) => onChange(propertyName, e.target.value)}
rows={3}
placeholder={description}
/>
)
}
return (
<Input
value={value || defaultValue || ''}
onChange={(e) => onChange(propertyName, e.target.value)}
placeholder={description}
/>
)
}
if (type === 'integer' && propertyName === 'seed') {
const generateRandomSeed = () => Math.floor(Math.random() * 1000000)
return (
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<InputNumber
style={{ flex: 1 }}
value={value || defaultValue}
onChange={(v) => onChange(propertyName, v)}
step={1}
min={schemaProperty.minimum}
max={schemaProperty.maximum}
/>
<Button
size="small"
icon={<RedoOutlined />}
onClick={() => onChange(propertyName, generateRandomSeed())}
title="Generate random seed"
/>
</div>
)
}
if (type === 'integer' || type === 'number') {
const step = type === 'number' ? 0.1 : 1
return (
<InputNumber
style={{ width: '100%' }}
value={value || defaultValue}
onChange={(v) => onChange(propertyName, v)}
step={step}
min={schemaProperty.minimum}
max={schemaProperty.maximum}
/>
)
}
if (type === 'boolean') {
return (
<Switch
checked={value !== undefined ? value : defaultValue}
onChange={(checked) => onChange(propertyName, checked)}
style={{ width: '2px' }}
/>
)
}
return null
}
@@ -0,0 +1,27 @@
import type { TokenFluxPainting } from '@renderer/types'
import { uuid } from '@renderer/utils'
export interface TokenFluxModel {
id: string
name: string
model_provider: string
description: string
tags: string[]
pricing: any
input_schema: {
type: string
properties: Record<string, any>
required: string[]
}
}
export const DEFAULT_TOKENFLUX_PAINTING: TokenFluxPainting = {
id: uuid(),
model: '',
prompt: '',
inputParams: {},
status: 'starting',
generationId: undefined,
urls: [],
files: []
}
@@ -0,0 +1,237 @@
import { CacheService } from '@renderer/services/CacheService'
import { FileType, TokenFluxPainting } from '@renderer/types'
import type { TokenFluxModel } from '../config/tokenFluxConfig'
export interface TokenFluxGenerationRequest {
model: string
input: {
prompt: string
[key: string]: any
}
}
export interface TokenFluxGenerationResponse {
success: boolean
data?: {
id: string
status: string
images?: Array<{ url: string }>
}
message?: string
}
export interface TokenFluxModelsResponse {
success: boolean
data?: TokenFluxModel[]
message?: string
}
export class TokenFluxService {
private apiHost: string
private apiKey: string
constructor(apiHost: string, apiKey: string) {
this.apiHost = apiHost
this.apiKey = apiKey
}
private getHeaders(): Record<string, string> {
return {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
}
private async handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorData = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(errorData.message || `HTTP ${response.status}: Request failed`)
}
return response.json()
}
/**
* Fetch available models from TokenFlux API
*/
async fetchModels(): Promise<TokenFluxModel[]> {
const cacheKey = `tokenflux_models_${this.apiHost}`
// Check cache first
const cachedModels = CacheService.get<TokenFluxModel[]>(cacheKey)
if (cachedModels) {
return cachedModels
}
const response = await fetch(`${this.apiHost}/v1/images/models`, {
headers: {
Authorization: `Bearer ${this.apiKey}`
}
})
const data: TokenFluxModelsResponse = await this.handleResponse(response)
if (!data.success || !data.data) {
throw new Error('Failed to fetch models')
}
// Cache for 60 minutes (3,600,000 milliseconds)
CacheService.set(cacheKey, data.data, 60 * 60 * 1000)
return data.data
}
/**
* Create a new image generation request
*/
async createGeneration(request: TokenFluxGenerationRequest, signal?: AbortSignal): Promise<string> {
const response = await fetch(`${this.apiHost}/v1/images/generations`, {
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify(request),
signal
})
const data: TokenFluxGenerationResponse = await this.handleResponse(response)
if (!data.success || !data.data?.id) {
throw new Error(data.message || 'Generation failed')
}
return data.data.id
}
/**
* Get the status and result of a generation
*/
async getGenerationResult(generationId: string): Promise<TokenFluxGenerationResponse['data']> {
const response = await fetch(`${this.apiHost}/v1/images/generations/${generationId}`, {
headers: {
Authorization: `Bearer ${this.apiKey}`
}
})
const data: TokenFluxGenerationResponse = await this.handleResponse(response)
if (!data.success || !data.data) {
throw new Error('Invalid response from generation service')
}
return data.data
}
/**
* Poll for generation result with automatic retry logic
*/
async pollGenerationResult(
generationId: string,
options: {
onStatusUpdate?: (updates: Partial<TokenFluxPainting>) => void
maxRetries?: number
timeoutMs?: number
intervalMs?: number
} = {}
): Promise<TokenFluxGenerationResponse['data']> {
const {
onStatusUpdate,
maxRetries = 10,
timeoutMs = 120000, // 2 minutes
intervalMs = 2000
} = options
const startTime = Date.now()
let retryCount = 0
return new Promise((resolve, reject) => {
const poll = async () => {
try {
// Check for timeout
if (Date.now() - startTime > timeoutMs) {
reject(new Error('Image generation timed out. Please try again.'))
return
}
const result = await this.getGenerationResult(generationId)
// Reset retry count on successful response
retryCount = 0
if (result) {
onStatusUpdate?.({ status: result.status as TokenFluxPainting['status'] })
if (result.status === 'succeeded') {
resolve(result)
return
} else if (result.status === 'failed') {
reject(new Error('Image generation failed'))
return
}
}
// Continue polling for other statuses (processing, queued, etc.)
setTimeout(poll, intervalMs)
} catch (error) {
console.error('Polling error:', error)
retryCount++
if (retryCount >= maxRetries) {
reject(new Error('Failed to check generation status after multiple attempts. Please try again.'))
return
}
// Retry after interval
setTimeout(poll, intervalMs)
}
}
// Start polling
poll()
})
}
/**
* Create generation and poll for result in one call
*/
async generateAndWait(
request: TokenFluxGenerationRequest,
options: {
onStatusUpdate?: (updates: Partial<TokenFluxPainting>) => void
signal?: AbortSignal
maxRetries?: number
timeoutMs?: number
intervalMs?: number
} = {}
): Promise<TokenFluxGenerationResponse['data']> {
const { signal, onStatusUpdate, ...pollOptions } = options
const generationId = await this.createGeneration(request, signal)
if (onStatusUpdate) {
onStatusUpdate({ generationId })
}
return this.pollGenerationResult(generationId, { ...pollOptions, onStatusUpdate })
}
async downloadImages(urls: string[]) {
const downloadedFiles = await Promise.all(
urls.map(async (url) => {
try {
if (!url?.trim()) {
console.error('Image URL is empty')
window.message.warning({
content: 'Image URL is empty',
key: 'empty-url-warning'
})
return null
}
return await window.api.file.download(url)
} catch (error) {
console.error('Failed to download image:', error)
return null
}
})
)
return downloadedFiles.filter((file): file is FileType => file !== null)
}
}
export default TokenFluxService
@@ -13,6 +13,8 @@ import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import styled from 'styled-components'
import { SettingDivider } from '..'
interface Props {
assistant: Assistant
updateAssistant: (assistant: Assistant) => void
@@ -90,7 +92,8 @@ const AssistantPromptSettings: React.FC<Props> = ({ assistant, updateAssistant }
style={{ flex: 1 }}
/>
</HStack>
<HStack mt={8} mb={8} alignItems="center" gap={4}>
<SettingDivider />
<HStack mb={8} alignItems="center" gap={4}>
<Box style={{ fontWeight: 'bold' }}>{t('common.prompt')}</Box>
<Tooltip title={t('agents.add.prompt.variables.tip')}>
<QuestionCircleOutlined size={14} color="var(--color-text-2)" />
@@ -139,7 +142,6 @@ const Container = styled.div`
flex: 1;
flex-direction: column;
overflow: hidden;
padding: 5px;
`
const EmojiButtonWrapper = styled.div`
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { v4 as uuidv4 } from 'uuid'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitle } from '..'
import { SettingDivider, SettingRow, SettingTitle } from '..'
const { TextArea } = Input
@@ -79,52 +79,50 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
const reversedPrompts = [...promptsList].reverse()
return (
<SettingContainer style={{ padding: 0, background: '#0000' }}>
<SettingGroup style={{ marginBottom: 0, padding: 0, border: 'none' }}>
<SettingTitle>
{t('assistants.settings.regular_phrases.title', 'Regular Prompts')}
<Button type="text" icon={<PlusOutlined />} onClick={handleAdd} />
</SettingTitle>
<SettingDivider />
<SettingRow>
<StyledPromptList>
<DragableList
list={reversedPrompts}
onUpdate={(newPrompts) => handleUpdateOrder([...newPrompts].reverse())}
style={{ paddingBottom: dragging ? '34px' : 0 }}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(prompt) => (
<FileItem
key={prompt.id}
fileInfo={{
name: prompt.title,
ext: '.txt',
extra: prompt.content,
actions: (
<Flex gap={4} style={{ opacity: 0.6 }}>
<Button key="edit" type="text" icon={<EditOutlined />} onClick={() => handleEdit(prompt)} />
<Popconfirm
title={t('assistants.settings.regular_phrases.delete', 'Delete Prompt')}
description={t(
'assistants.settings.regular_phrases.deleteConfirm',
'Are you sure to delete this prompt?'
)}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
onConfirm={() => handleDelete(prompt.id)}
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
<Button key="delete" type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Flex>
)
}}
/>
)}
</DragableList>
</StyledPromptList>
</SettingRow>
</SettingGroup>
<Container>
<SettingTitle>
{t('assistants.settings.regular_phrases.title', 'Regular Prompts')}
<Button type="text" icon={<PlusOutlined />} onClick={handleAdd} />
</SettingTitle>
<SettingDivider />
<SettingRow>
<StyledPromptList>
<DragableList
list={reversedPrompts}
onUpdate={(newPrompts) => handleUpdateOrder([...newPrompts].reverse())}
style={{ paddingBottom: dragging ? '34px' : 0 }}
onDragStart={() => setDragging(true)}
onDragEnd={() => setDragging(false)}>
{(prompt) => (
<FileItem
key={prompt.id}
fileInfo={{
name: prompt.title,
ext: '.txt',
extra: prompt.content,
actions: (
<Flex gap={4} style={{ opacity: 0.6 }}>
<Button key="edit" type="text" icon={<EditOutlined />} onClick={() => handleEdit(prompt)} />
<Popconfirm
title={t('assistants.settings.regular_phrases.delete', 'Delete Prompt')}
description={t(
'assistants.settings.regular_phrases.deleteConfirm',
'Are you sure to delete this prompt?'
)}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
onConfirm={() => handleDelete(prompt.id)}
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
<Button key="delete" type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
</Flex>
)
}}
/>
)}
</DragableList>
</StyledPromptList>
</SettingRow>
<Modal
title={
@@ -159,10 +157,16 @@ const AssistantRegularPromptsSettings: FC<AssistantRegularPromptsSettingsProps>
</div>
</Space>
</Modal>
</SettingContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
`
const Label = styled.div`
font-size: 14px;
color: var(--color-text);
@@ -171,8 +175,6 @@ const Label = styled.div`
const StyledPromptList = styled.div`
width: 100%;
height: calc(100vh - 162px); // Adjusted height to match other settings pages
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
@@ -55,7 +55,7 @@ import mime from 'mime'
import OpenAI from 'openai'
import { ChatCompletionContentPart, ChatCompletionMessageParam } from 'openai/resources/chat/completions'
import { Stream } from 'openai/streaming'
import { FileLike, toFile } from 'openai/uploads'
import { toFile, Uploadable } from 'openai/uploads'
import { CompletionsParams } from '.'
import BaseProvider from './BaseProvider'
@@ -1052,7 +1052,7 @@ export abstract class BaseOpenAIProvider extends BaseProvider {
const { signal } = abortController
const content = getMainTextContent(lastUserMessage!)
let response: OpenAI.Images.ImagesResponse | null = null
let images: FileLike[] = []
let images: Uploadable[] = []
try {
if (lastUserMessage) {
@@ -1084,7 +1084,7 @@ export abstract class BaseOpenAIProvider extends BaseProvider {
return await toFile(bytes, fileName, { type: mimeType })
})
)
images = images.concat(assistantImages.filter(Boolean) as FileLike[])
images = images.concat(assistantImages.filter(Boolean) as Uploadable[])
}
onChunk({
+30 -30
View File
@@ -66,16 +66,6 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'openrouter',
name: 'OpenRouter',
type: 'openai',
apiKey: '',
apiHost: 'https://openrouter.ai/api/v1/',
models: SYSTEM_MODELS.openrouter,
isSystem: true,
enabled: false
},
{
id: 'ppio',
name: 'PPIO',
@@ -96,16 +86,6 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'infini',
name: 'Infini',
type: 'openai',
apiKey: '',
apiHost: 'https://cloud.infini-ai.com/maas',
models: SYSTEM_MODELS.infini,
isSystem: true,
enabled: false
},
{
id: 'qiniu',
name: 'Qiniu',
@@ -136,6 +116,16 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'tokenflux',
name: 'TokenFlux',
type: 'openai',
apiKey: '',
apiHost: 'https://tokenflux.ai',
models: SYSTEM_MODELS.tokenflux,
isSystem: true,
enabled: false
},
{
id: 'o3',
name: 'O3',
@@ -146,6 +136,16 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'openrouter',
name: 'OpenRouter',
type: 'openai',
apiKey: '',
apiHost: 'https://openrouter.ai/api/v1/',
models: SYSTEM_MODELS.openrouter,
isSystem: true,
enabled: false
},
{
id: 'ollama',
name: 'Ollama',
@@ -298,6 +298,16 @@ export const INITIAL_PROVIDERS: Provider[] = [
isSystem: true,
enabled: false
},
{
id: 'infini',
name: 'Infini',
type: 'openai',
apiKey: '',
apiHost: 'https://cloud.infini-ai.com/maas',
models: SYSTEM_MODELS.infini,
isSystem: true,
enabled: false
},
{
id: 'minimax',
name: 'MiniMax',
@@ -477,16 +487,6 @@ export const INITIAL_PROVIDERS: Provider[] = [
models: SYSTEM_MODELS.voyageai,
isSystem: true,
enabled: false
},
{
id: 'tokenflux',
name: 'TokenFlux',
type: 'openai',
apiKey: '',
apiHost: 'https://tokenflux.ai',
models: SYSTEM_MODELS.tokenflux,
isSystem: true,
enabled: false
}
]
+19 -9
View File
@@ -1475,7 +1475,23 @@ const migrateConfig = {
},
'110': (state: RootState) => {
try {
if (!state.preprocess) {
state.llm.providers.forEach((provider) => {
if (provider.id === 'mistral') {
provider.type = 'mistral'
}
})
if (state.paintings && !state.paintings.tokenFluxPaintings) {
state.paintings.tokenFluxPaintings = []
}
state.settings.showTokens = true
return state
} catch (error) {
return state
}
},
'111': (state: RootState) => {
try{
if (!state.preprocess) {
state.preprocess = {
defaultProvider: '',
providers: []
@@ -1515,15 +1531,9 @@ const migrateConfig = {
}
})
}
state.llm.providers.forEach((provider) => {
if (provider.id === 'mistral') {
provider.type = 'mistral'
}
})
state.settings.showTokens = true
return state
} catch (error) {
}catch(error) {
console.error(error)
return state
}
}
+9 -2
View File
@@ -7,7 +7,8 @@ const initialState: PaintingsState = {
remix: [],
edit: [],
upscale: [],
DMXAPIPaintings: []
DMXAPIPaintings: [],
tokenFluxPaintings: []
}
const paintingsSlice = createSlice({
@@ -38,7 +39,13 @@ const paintingsSlice = createSlice({
action: PayloadAction<{ namespace?: keyof PaintingsState; painting: PaintingAction }>
) => {
const { namespace = 'paintings', painting } = action.payload
state[namespace] = state[namespace].map((c) => (c.id === painting.id ? painting : c))
const existingIndex = state[namespace].findIndex((c) => c.id === painting.id)
if (existingIndex !== -1) {
state[namespace] = state[namespace].map((c) => (c.id === painting.id ? painting : c))
} else {
console.error(`Painting with id ${painting.id} not found in ${namespace}`)
}
},
updatePaintings: (
state: PaintingsState,
+13 -1
View File
@@ -271,7 +271,18 @@ export interface DmxapiPainting extends PaintingParams {
autoCreate?: boolean
}
export type PaintingAction = Partial<GeneratePainting & RemixPainting & EditPainting & ScalePainting> & PaintingParams
export interface TokenFluxPainting extends PaintingParams {
generationId?: string
model?: string
prompt?: string
inputParams?: Record<string, any>
status?: 'starting' | 'processing' | 'succeeded' | 'failed' | 'cancelled'
}
export type PaintingAction = Partial<
GeneratePainting & RemixPainting & EditPainting & ScalePainting & DmxapiPainting & TokenFluxPainting
> &
PaintingParams
export interface PaintingsState {
paintings: Painting[]
@@ -280,6 +291,7 @@ export interface PaintingsState {
edit: Partial<EditPainting> & PaintingParams[]
upscale: Partial<ScalePainting> & PaintingParams[]
DMXAPIPaintings: DmxapiPainting[]
tokenFluxPaintings: TokenFluxPainting[]
}
export type MinAppType = {
@@ -147,10 +147,12 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
}, [demo, isCompact, actionItems])
useEffect(() => {
i18n.changeLanguage(language || navigator.language || defaultLanguage)
}, [language])
!demo && i18n.changeLanguage(language || navigator.language || defaultLanguage)
}, [language, demo])
useEffect(() => {
if (demo) return
let customCssElement = document.getElementById('user-defined-custom-css') as HTMLStyleElement
if (customCssElement) {
customCssElement.remove()
@@ -164,7 +166,7 @@ const SelectionToolbar: FC<{ demo?: boolean }> = ({ demo = false }) => {
customCssElement.textContent = newCustomCss
document.head.appendChild(customCssElement)
}
}, [customCss])
}, [customCss, demo])
const onHideCleanUp = () => {
setCopyIconStatus('normal')
@@ -265,6 +267,7 @@ const LogoWrapper = styled.div`
justify-content: center;
-webkit-app-region: drag;
margin-left: 5px;
background-color: transparent;
`
const Logo = styled(Avatar)`
@@ -295,6 +298,7 @@ const ActionWrapper = styled.div`
align-items: center;
justify-content: center;
margin-left: 3px;
background-color: transparent;
`
const ActionButton = styled.div`
display: flex;
@@ -302,6 +306,7 @@ const ActionButton = styled.div`
align-items: center;
justify-content: center;
margin: 0 2px;
background-color: transparent;
cursor: pointer;
border-radius: 4px;
padding: 4px 6px;
@@ -309,6 +314,7 @@ const ActionButton = styled.div`
width: 16px;
height: 16px;
color: var(--color-selection-toolbar-text);
background-color: transparent;
}
.btn-title {
color: var(--color-selection-toolbar-text);
@@ -329,10 +335,10 @@ const ActionIcon = styled.div`
display: flex;
align-items: center;
justify-content: center;
/* margin-right: 3px; */
position: relative;
height: 16px;
width: 16px;
background-color: transparent;
.btn-icon {
position: absolute;
@@ -416,6 +422,7 @@ const ActionTitle = styled.span`
text-overflow: ellipsis;
white-space: nowrap;
margin-left: 3px;
background-color: transparent;
`
export default SelectionToolbar
+132 -33
View File
@@ -2990,6 +2990,25 @@ __metadata:
languageName: node
linkType: hard
"@mapbox/node-pre-gyp@npm:^1.0.0":
version: 1.0.11
resolution: "@mapbox/node-pre-gyp@npm:1.0.11"
dependencies:
detect-libc: "npm:^2.0.0"
https-proxy-agent: "npm:^5.0.0"
make-dir: "npm:^3.1.0"
node-fetch: "npm:^2.6.7"
nopt: "npm:^5.0.0"
npmlog: "npm:^5.0.1"
rimraf: "npm:^3.0.2"
semver: "npm:^7.3.5"
tar: "npm:^6.1.11"
bin:
node-pre-gyp: bin/node-pre-gyp
checksum: 10c0/2b24b93c31beca1c91336fa3b3769fda98e202fb7f9771f0f4062588d36dcc30fcf8118c36aa747fa7f7610d8cf601872bdaaf62ce7822bb08b545d1bbe086cc
languageName: node
linkType: hard
"@marijn/find-cluster-break@npm:^1.0.0":
version: 1.0.2
resolution: "@marijn/find-cluster-break@npm:1.0.2"
@@ -5664,7 +5683,7 @@ __metadata:
node-stream-zip: "npm:^1.15.0"
npx-scope-finder: "npm:^1.2.0"
officeparser: "npm:^4.1.1"
openai: "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch"
openai: "patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch"
os-proxy-config: "npm:^1.1.2"
p-queue: "npm:^8.1.0"
pdf-to-img: "npm:^4.4.0"
@@ -5714,7 +5733,7 @@ __metadata:
languageName: unknown
linkType: soft
"abbrev@npm:^1.0.0":
"abbrev@npm:1, abbrev@npm:^1.0.0":
version: 1.1.1
resolution: "abbrev@npm:1.1.1"
checksum: 10c0/3f762677702acb24f65e813070e306c61fafe25d4b2583f9dfc935131f774863f3addd5741572ed576bd69cabe473c5af18e1e108b829cb7b6b4747884f726e6
@@ -6069,6 +6088,13 @@ __metadata:
languageName: node
linkType: hard
"aproba@npm:^1.0.3 || ^2.0.0":
version: 2.0.0
resolution: "aproba@npm:2.0.0"
checksum: 10c0/d06e26384a8f6245d8c8896e138c0388824e259a329e0c9f196b4fa533c82502a6fd449586e3604950a0c42921832a458bb3aa0aa9f0ba449cfd4f50fd0d09b5
languageName: node
linkType: hard
"archiver-utils@npm:^5.0.0, archiver-utils@npm:^5.0.2":
version: 5.0.2
resolution: "archiver-utils@npm:5.0.2"
@@ -6099,6 +6125,16 @@ __metadata:
languageName: node
linkType: hard
"are-we-there-yet@npm:^2.0.0":
version: 2.0.0
resolution: "are-we-there-yet@npm:2.0.0"
dependencies:
delegates: "npm:^1.0.0"
readable-stream: "npm:^3.6.0"
checksum: 10c0/375f753c10329153c8d66dc95e8f8b6c7cc2aa66e05cb0960bd69092b10dae22900cacc7d653ad11d26b3ecbdbfe1e8bfb6ccf0265ba8077a7d979970f16b99c
languageName: node
linkType: hard
"are-we-there-yet@npm:~1.1.2":
version: 1.1.7
resolution: "are-we-there-yet@npm:1.1.7"
@@ -6692,6 +6728,18 @@ __metadata:
languageName: node
linkType: hard
"canvas@npm:^2.11.2":
version: 2.11.2
resolution: "canvas@npm:2.11.2"
dependencies:
"@mapbox/node-pre-gyp": "npm:^1.0.0"
nan: "npm:^2.17.0"
node-gyp: "npm:latest"
simple-get: "npm:^3.0.3"
checksum: 10c0/943368798ad1b66b18633aa34b6181e1038dac5433fc9727cd07be35f0a633f572b60d9edb95f5ff90b6a9128e86d5312035f91a2934101c73185b15d906230a
languageName: node
linkType: hard
"ccount@npm:^1.0.0":
version: 1.1.0
resolution: "ccount@npm:1.1.0"
@@ -7052,6 +7100,15 @@ __metadata:
languageName: node
linkType: hard
"color-support@npm:^1.1.2":
version: 1.1.3
resolution: "color-support@npm:1.1.3"
bin:
color-support: bin.js
checksum: 10c0/8ffeaa270a784dc382f62d9be0a98581db43e11eee301af14734a6d089bd456478b1a8b3e7db7ca7dc5b18a75f828f775c44074020b51c05fc00e6d0992b1cc6
languageName: node
linkType: hard
"color@npm:^5.0.0":
version: 5.0.0
resolution: "color@npm:5.0.0"
@@ -7203,7 +7260,7 @@ __metadata:
languageName: node
linkType: hard
"console-control-strings@npm:^1.0.0, console-control-strings@npm:~1.1.0":
"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0, console-control-strings@npm:~1.1.0":
version: 1.1.0
resolution: "console-control-strings@npm:1.1.0"
checksum: 10c0/7ab51d30b52d461412cd467721bb82afe695da78fff8f29fe6f6b9cbaac9a2328e27a22a966014df9532100f6dd85370460be8130b9c677891ba36d96a343f50
@@ -10022,6 +10079,23 @@ __metadata:
languageName: node
linkType: hard
"gauge@npm:^3.0.0":
version: 3.0.2
resolution: "gauge@npm:3.0.2"
dependencies:
aproba: "npm:^1.0.3 || ^2.0.0"
color-support: "npm:^1.1.2"
console-control-strings: "npm:^1.0.0"
has-unicode: "npm:^2.0.1"
object-assign: "npm:^4.1.1"
signal-exit: "npm:^3.0.0"
string-width: "npm:^4.2.3"
strip-ansi: "npm:^6.0.1"
wide-align: "npm:^1.1.2"
checksum: 10c0/75230ccaf216471e31025c7d5fcea1629596ca20792de50c596eb18ffb14d8404f927cd55535aab2eeecd18d1e11bd6f23ec3c2e9878d2dda1dc74bccc34b913
languageName: node
linkType: hard
"gauge@npm:~2.7.3":
version: 2.7.4
resolution: "gauge@npm:2.7.4"
@@ -10404,7 +10478,7 @@ __metadata:
languageName: node
linkType: hard
"has-unicode@npm:^2.0.0":
"has-unicode@npm:^2.0.0, has-unicode@npm:^2.0.1":
version: 2.0.1
resolution: "has-unicode@npm:2.0.1"
checksum: 10c0/ebdb2f4895c26bb08a8a100b62d362e49b2190bcfd84b76bc4be1a3bd4d254ec52d0dd9f2fbcc093fc5eb878b20c52146f9dfd33e2686ed28982187be593b47c
@@ -12146,6 +12220,15 @@ __metadata:
languageName: node
linkType: hard
"make-dir@npm:^3.1.0":
version: 3.1.0
resolution: "make-dir@npm:3.1.0"
dependencies:
semver: "npm:^6.0.0"
checksum: 10c0/56aaafefc49c2dfef02c5c95f9b196c4eb6988040cf2c712185c7fe5c99b4091591a7fc4d4eafaaefa70ff763a26f6ab8c3ff60b9e75ea19876f49b18667ecaa
languageName: node
linkType: hard
"make-dir@npm:^4.0.0":
version: 4.0.0
resolution: "make-dir@npm:4.0.0"
@@ -13631,6 +13714,15 @@ __metadata:
languageName: node
linkType: hard
"nan@npm:^2.17.0":
version: 2.22.2
resolution: "nan@npm:2.22.2"
dependencies:
node-gyp: "npm:latest"
checksum: 10c0/971f963b8120631880fa47a389c71b00cadc1c1b00ef8f147782a3f4387d4fc8195d0695911272d57438c11562fb27b24c4ae5f8c05d5e4eeb4478ba51bb73c5
languageName: node
linkType: hard
"nanoid@npm:^3.3.7, nanoid@npm:^3.3.8":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"
@@ -13859,6 +13951,17 @@ __metadata:
languageName: node
linkType: hard
"nopt@npm:^5.0.0":
version: 5.0.0
resolution: "nopt@npm:5.0.0"
dependencies:
abbrev: "npm:1"
bin:
nopt: bin/nopt.js
checksum: 10c0/fc5c4f07155cb455bf5fc3dd149fac421c1a40fd83c6bfe83aa82b52f02c17c5e88301321318adaa27611c8a6811423d51d29deaceab5fa158b585a61a551061
languageName: node
linkType: hard
"nopt@npm:^6.0.0":
version: 6.0.0
resolution: "nopt@npm:6.0.0"
@@ -13923,6 +14026,18 @@ __metadata:
languageName: node
linkType: hard
"npmlog@npm:^5.0.1":
version: 5.0.1
resolution: "npmlog@npm:5.0.1"
dependencies:
are-we-there-yet: "npm:^2.0.0"
console-control-strings: "npm:^1.1.0"
gauge: "npm:^3.0.0"
set-blocking: "npm:^2.0.0"
checksum: 10c0/489ba519031013001135c463406f55491a17fc7da295c18a04937fe3a4d523fd65e88dd418a28b967ab743d913fdeba1e29838ce0ad8c75557057c481f7d49fa
languageName: node
linkType: hard
"npx-scope-finder@npm:^1.2.0":
version: 1.3.0
resolution: "npx-scope-finder@npm:1.3.0"
@@ -13944,7 +14059,7 @@ __metadata:
languageName: node
linkType: hard
"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0":
"object-assign@npm:^4, object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1":
version: 4.1.1
resolution: "object-assign@npm:4.1.1"
checksum: 10c0/1f4df9945120325d041ccf7b86f31e8bcc14e73d29171e37a7903050e96b81323784ec59f93f102ec635bcf6fa8034ba3ea0a8c7e69fa202b87ae3b6cec5a414
@@ -14080,17 +14195,9 @@ __metadata:
languageName: node
linkType: hard
"openai@npm:4.96.0":
version: 4.96.0
resolution: "openai@npm:4.96.0"
dependencies:
"@types/node": "npm:^18.11.18"
"@types/node-fetch": "npm:^2.6.4"
abort-controller: "npm:^3.0.0"
agentkeepalive: "npm:^4.2.1"
form-data-encoder: "npm:1.7.2"
formdata-node: "npm:^4.3.2"
node-fetch: "npm:^2.6.7"
"openai@npm:5.1.0":
version: 5.1.0
resolution: "openai@npm:5.1.0"
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
@@ -14101,21 +14208,13 @@ __metadata:
optional: true
bin:
openai: bin/cli
checksum: 10c0/d4c3fa76374730c856f774e07f375b51041b8e8429ae2cbd8605b168bf81673017f5dd1c0e42419ca54d8d3fd7cd93d57830d6bc6b9dcd317e70109018d599ea
checksum: 10c0/d5882c95f95bfc4127ccbe494d298f43fe56cd3a9fd5711d1f02a040cfa6cdcc1e706ffe05f3d428421ec4caa526fe1b5ff50e1849dbfb3016d289853262ea3d
languageName: node
linkType: hard
"openai@patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch":
version: 4.96.0
resolution: "openai@patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch::version=4.96.0&hash=6bc976"
dependencies:
"@types/node": "npm:^18.11.18"
"@types/node-fetch": "npm:^2.6.4"
abort-controller: "npm:^3.0.0"
agentkeepalive: "npm:^4.2.1"
form-data-encoder: "npm:1.7.2"
formdata-node: "npm:^4.3.2"
node-fetch: "npm:^2.6.7"
"openai@patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch":
version: 5.1.0
resolution: "openai@patch:openai@npm%3A5.1.0#~/.yarn/patches/openai-npm-5.1.0-0e7b3ccb07.patch::version=5.1.0&hash=cf4b11"
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
@@ -14126,7 +14225,7 @@ __metadata:
optional: true
bin:
openai: bin/cli
checksum: 10c0/e50e4b9b60e94fadaca541cf2c36a12c55221555dd2ce977738e13978b7187504263f2e31b4641f2b6e70fce562b4e1fa2affd68caeca21248ddfa8847eeb003
checksum: 10c0/26abab8311c5e130759d8b2a939ac408872d808c8e8b8f6a7bb5c85f2df0a66d61aece3af783dbf04a2aa401481cf20a6eddcb777b545b387f66220a6a6d25d7
languageName: node
linkType: hard
@@ -15825,7 +15924,7 @@ __metadata:
languageName: node
linkType: hard
"readable-stream@npm:3, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0":
"readable-stream@npm:3, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0":
version: 3.6.2
resolution: "readable-stream@npm:3.6.2"
dependencies:
@@ -16569,7 +16668,7 @@ __metadata:
languageName: node
linkType: hard
"semver@npm:^6.2.0, semver@npm:^6.3.1":
"semver@npm:^6.0.0, semver@npm:^6.2.0, semver@npm:^6.3.1":
version: 6.3.1
resolution: "semver@npm:6.3.1"
bin:
@@ -16627,7 +16726,7 @@ __metadata:
languageName: node
linkType: hard
"set-blocking@npm:~2.0.0":
"set-blocking@npm:^2.0.0, set-blocking@npm:~2.0.0":
version: 2.0.0
resolution: "set-blocking@npm:2.0.0"
checksum: 10c0/9f8c1b2d800800d0b589de1477c753492de5c1548d4ade52f57f1d1f5e04af5481554d75ce5e5c43d4004b80a3eb714398d6907027dc0534177b7539119f4454
@@ -18687,7 +18786,7 @@ __metadata:
languageName: node
linkType: hard
"wide-align@npm:^1.1.0":
"wide-align@npm:^1.1.0, wide-align@npm:^1.1.2":
version: 1.1.5
resolution: "wide-align@npm:1.1.5"
dependencies: