mirror of
				https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
				synced 2025-10-31 14:23:43 +08:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | bc1794fb4a | ||
|  | ff166f7b4c | ||
|  | e756506c18 | ||
|  | fd67f980a5 | ||
|  | e2da3406d2 | ||
|  | 05b6d989b6 | ||
|  | 1d6ee64e1d | 
							
								
								
									
										43
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										43
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,43 +0,0 @@ | |||||||
| --- |  | ||||||
| name: Bug report |  | ||||||
| about: Create a report to help us improve |  | ||||||
| title: "[Bug] " |  | ||||||
| labels: '' |  | ||||||
| assignees: '' |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| **Describe the bug** |  | ||||||
| A clear and concise description of what the bug is. |  | ||||||
|  |  | ||||||
| **To Reproduce** |  | ||||||
| Steps to reproduce the behavior: |  | ||||||
| 1. Go to '...' |  | ||||||
| 2. Click on '....' |  | ||||||
| 3. Scroll down to '....' |  | ||||||
| 4. See error |  | ||||||
|  |  | ||||||
| **Expected behavior** |  | ||||||
| A clear and concise description of what you expected to happen. |  | ||||||
|  |  | ||||||
| **Screenshots** |  | ||||||
| If applicable, add screenshots to help explain your problem. |  | ||||||
|  |  | ||||||
| **Deployment** |  | ||||||
| - [ ] Docker |  | ||||||
| - [ ] Vercel |  | ||||||
| - [ ] Server |  | ||||||
|  |  | ||||||
| **Desktop (please complete the following information):** |  | ||||||
|  - OS: [e.g. iOS] |  | ||||||
|  - Browser [e.g. chrome, safari] |  | ||||||
|  - Version [e.g. 22] |  | ||||||
|  |  | ||||||
| **Smartphone (please complete the following information):** |  | ||||||
|  - Device: [e.g. iPhone6] |  | ||||||
|  - OS: [e.g. iOS8.1] |  | ||||||
|  - Browser [e.g. stock browser, safari] |  | ||||||
|  - Version [e.g. 22] |  | ||||||
|  |  | ||||||
| **Additional Logs** |  | ||||||
| Add any logs about the problem here. |  | ||||||
							
								
								
									
										146
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | |||||||
|  | name: Bug report | ||||||
|  | description: Create a report to help us improve | ||||||
|  | title: "[Bug] " | ||||||
|  | labels: ["bug"] | ||||||
|  |  | ||||||
|  | body: | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Describe the bug" | ||||||
|  |   - type: textarea | ||||||
|  |     id: bug-description | ||||||
|  |     attributes: | ||||||
|  |       label: "Bug Description" | ||||||
|  |       description: "A clear and concise description of what the bug is." | ||||||
|  |       placeholder: "Explain the bug..." | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## To Reproduce" | ||||||
|  |   - type: textarea | ||||||
|  |     id: steps-to-reproduce | ||||||
|  |     attributes: | ||||||
|  |       label: "Steps to Reproduce" | ||||||
|  |       description: "Steps to reproduce the behavior:" | ||||||
|  |       placeholder: | | ||||||
|  |         1. Go to '...' | ||||||
|  |         2. Click on '....' | ||||||
|  |         3. Scroll down to '....' | ||||||
|  |         4. See error | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Expected behavior" | ||||||
|  |   - type: textarea | ||||||
|  |     id: expected-behavior | ||||||
|  |     attributes: | ||||||
|  |       label: "Expected Behavior" | ||||||
|  |       description: "A clear and concise description of what you expected to happen." | ||||||
|  |       placeholder: "Describe what you expected to happen..." | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Screenshots" | ||||||
|  |   - type: textarea | ||||||
|  |     id: screenshots | ||||||
|  |     attributes: | ||||||
|  |       label: "Screenshots" | ||||||
|  |       description: "If applicable, add screenshots to help explain your problem." | ||||||
|  |       placeholder: "Paste your screenshots here or write 'N/A' if not applicable..." | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Deployment" | ||||||
|  |   - type: checkboxes | ||||||
|  |     id: deployment | ||||||
|  |     attributes: | ||||||
|  |       label: "Deployment Method" | ||||||
|  |       description: "Please select the deployment method you are using." | ||||||
|  |       options: | ||||||
|  |         - label: "Docker" | ||||||
|  |         - label: "Vercel" | ||||||
|  |         - label: "Server" | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Desktop (please complete the following information):" | ||||||
|  |   - type: input | ||||||
|  |     id: desktop-os | ||||||
|  |     attributes: | ||||||
|  |       label: "Desktop OS" | ||||||
|  |       description: "Your desktop operating system." | ||||||
|  |       placeholder: "e.g., Windows 10" | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
|  |   - type: input | ||||||
|  |     id: desktop-browser | ||||||
|  |     attributes: | ||||||
|  |       label: "Desktop Browser" | ||||||
|  |       description: "Your desktop browser." | ||||||
|  |       placeholder: "e.g., Chrome, Safari" | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
|  |   - type: input | ||||||
|  |     id: desktop-version | ||||||
|  |     attributes: | ||||||
|  |       label: "Desktop Browser Version" | ||||||
|  |       description: "Version of your desktop browser." | ||||||
|  |       placeholder: "e.g., 89.0" | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Smartphone (please complete the following information):" | ||||||
|  |   - type: input | ||||||
|  |     id: smartphone-device | ||||||
|  |     attributes: | ||||||
|  |       label: "Smartphone Device" | ||||||
|  |       description: "Your smartphone device." | ||||||
|  |       placeholder: "e.g., iPhone X" | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
|  |   - type: input | ||||||
|  |     id: smartphone-os | ||||||
|  |     attributes: | ||||||
|  |       label: "Smartphone OS" | ||||||
|  |       description: "Your smartphone operating system." | ||||||
|  |       placeholder: "e.g., iOS 14.4" | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
|  |   - type: input | ||||||
|  |     id: smartphone-browser | ||||||
|  |     attributes: | ||||||
|  |       label: "Smartphone Browser" | ||||||
|  |       description: "Your smartphone browser." | ||||||
|  |       placeholder: "e.g., Safari" | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
|  |   - type: input | ||||||
|  |     id: smartphone-version | ||||||
|  |     attributes: | ||||||
|  |       label: "Smartphone Browser Version" | ||||||
|  |       description: "Version of your smartphone browser." | ||||||
|  |       placeholder: "e.g., 14" | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Additional Logs" | ||||||
|  |   - type: textarea | ||||||
|  |     id: additional-logs | ||||||
|  |     attributes: | ||||||
|  |       label: "Additional Logs" | ||||||
|  |       description: "Add any logs about the problem here." | ||||||
|  |       placeholder: "Paste any relevant logs here..." | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,20 +0,0 @@ | |||||||
| --- |  | ||||||
| name: Feature request |  | ||||||
| about: Suggest an idea for this project |  | ||||||
| title: "[Feature] " |  | ||||||
| labels: '' |  | ||||||
| assignees: '' |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| **Is your feature request related to a problem? Please describe.** |  | ||||||
| A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] |  | ||||||
|  |  | ||||||
| **Describe the solution you'd like** |  | ||||||
| A clear and concise description of what you want to happen. |  | ||||||
|  |  | ||||||
| **Describe alternatives you've considered** |  | ||||||
| A clear and concise description of any alternative solutions or features you've considered. |  | ||||||
|  |  | ||||||
| **Additional context** |  | ||||||
| Add any other context or screenshots about the feature request here. |  | ||||||
							
								
								
									
										53
									
								
								.github/ISSUE_TEMPLATE/feature_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								.github/ISSUE_TEMPLATE/feature_request.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | name: Feature request | ||||||
|  | description: Suggest an idea for this project | ||||||
|  | title: "[Feature Request]: " | ||||||
|  | labels: ["enhancement"] | ||||||
|  |  | ||||||
|  | body: | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Is your feature request related to a problem? Please describe." | ||||||
|  |   - type: textarea | ||||||
|  |     id: problem-description | ||||||
|  |     attributes: | ||||||
|  |       label: Problem Description | ||||||
|  |       description: "A clear and concise description of what the problem is. Example: I'm always frustrated when [...]" | ||||||
|  |       placeholder: "Explain the problem you are facing..." | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Describe the solution you'd like" | ||||||
|  |   - type: textarea | ||||||
|  |     id: desired-solution | ||||||
|  |     attributes: | ||||||
|  |       label: Solution Description | ||||||
|  |       description: A clear and concise description of what you want to happen. | ||||||
|  |       placeholder: "Describe the solution you'd like..." | ||||||
|  |     validations: | ||||||
|  |       required: true | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Describe alternatives you've considered" | ||||||
|  |   - type: textarea | ||||||
|  |     id: alternatives-considered | ||||||
|  |     attributes: | ||||||
|  |       label: Alternatives Considered | ||||||
|  |       description: A clear and concise description of any alternative solutions or features you've considered. | ||||||
|  |       placeholder: "Describe any alternative solutions or features you've considered..." | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
|  |  | ||||||
|  |   - type: markdown | ||||||
|  |     attributes: | ||||||
|  |       value: "## Additional context" | ||||||
|  |   - type: textarea | ||||||
|  |     id: additional-context | ||||||
|  |     attributes: | ||||||
|  |       label: Additional Context | ||||||
|  |       description: Add any other context or screenshots about the feature request here. | ||||||
|  |       placeholder: "Add any other context or screenshots about the feature request here..." | ||||||
|  |     validations: | ||||||
|  |       required: false | ||||||
							
								
								
									
										24
									
								
								.github/ISSUE_TEMPLATE/功能建议.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										24
									
								
								.github/ISSUE_TEMPLATE/功能建议.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,24 +0,0 @@ | |||||||
| --- |  | ||||||
| name: 功能建议 |  | ||||||
| about: 请告诉我们你的灵光一闪 |  | ||||||
| title: "[Feature] " |  | ||||||
| labels: '' |  | ||||||
| assignees: '' |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| > 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。 |  | ||||||
|  |  | ||||||
| > [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) |  | ||||||
|  |  | ||||||
| **这个功能与现有的问题有关吗?** |  | ||||||
| 如果有关,请在此列出链接或者描述问题。 |  | ||||||
|  |  | ||||||
| **你想要什么功能或者有什么建议?** |  | ||||||
| 尽管告诉我们。 |  | ||||||
|  |  | ||||||
| **有没有可以参考的同类竞品?** |  | ||||||
| 可以给出参考产品的链接或者截图。 |  | ||||||
|  |  | ||||||
| **其他信息** |  | ||||||
| 可以说说你的其他考虑。 |  | ||||||
							
								
								
									
										36
									
								
								.github/ISSUE_TEMPLATE/反馈问题.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								.github/ISSUE_TEMPLATE/反馈问题.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,36 +0,0 @@ | |||||||
| --- |  | ||||||
| name: 反馈问题 |  | ||||||
| about: 请告诉我们你遇到的问题 |  | ||||||
| title: "[Bug] " |  | ||||||
| labels: '' |  | ||||||
| assignees: '' |  | ||||||
|  |  | ||||||
| --- |  | ||||||
|  |  | ||||||
| > 为了提高交流效率,我们设立了官方 QQ 群和 QQ 频道,如果你在使用或者搭建过程中遇到了任何问题,请先第一时间加群或者频道咨询解决,除非是可以稳定复现的 Bug 或者较为有创意的功能建议,否则请不要随意往 Issue 区发送低质无意义帖子。 |  | ||||||
|  |  | ||||||
| > [点击加入官方群聊](https://github.com/Yidadaa/ChatGPT-Next-Web/discussions/1724) |  | ||||||
|  |  | ||||||
| **反馈须知** |  | ||||||
|  |  | ||||||
| ⚠️ 注意:不遵循此模板的任何帖子都会被立即关闭,如果没有提供下方的信息,我们无法定位你的问题。 |  | ||||||
|  |  | ||||||
| 请在下方中括号内输入 x 来表示你已经知晓相关内容。 |  | ||||||
| - [ ] 我确认已经在 [常见问题](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/faq-cn.md) 中搜索了此次反馈的问题,没有找到解答; |  | ||||||
| - [ ] 我确认已经在 [Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) 列表(包括已经 Close 的)中搜索了此次反馈的问题,没有找到解答。 |  | ||||||
| - [ ] 我确认已经在 [Vercel 使用教程](https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/docs/vercel-cn.md) 中搜索了此次反馈的问题,没有找到解答。 |  | ||||||
|  |  | ||||||
| **描述问题** |  | ||||||
| 请在此描述你遇到了什么问题。 |  | ||||||
|  |  | ||||||
| **如何复现** |  | ||||||
| 请告诉我们你是通过什么操作触发的该问题。 |  | ||||||
|  |  | ||||||
| **截图** |  | ||||||
| 请在此提供控制台截图、屏幕截图或者服务端的 log 截图。 |  | ||||||
|  |  | ||||||
| **一些必要的信息** |  | ||||||
|  - 系统:[比如 windows 10/ macos 12/ linux / android 11 / ios 16] |  | ||||||
|  - 浏览器: [比如 chrome, safari] |  | ||||||
|  - 版本: [填写设置页面的版本号] |  | ||||||
|  - 部署方式:[比如 vercel、docker 或者服务器部署] |  | ||||||
| @@ -14,9 +14,17 @@ export type MessageRole = (typeof ROLES)[number]; | |||||||
| export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; | export const Models = ["gpt-3.5-turbo", "gpt-4"] as const; | ||||||
| export type ChatModel = ModelType; | export type ChatModel = ModelType; | ||||||
|  |  | ||||||
|  | export interface MultimodalContent { | ||||||
|  |   type: "text" | "image_url"; | ||||||
|  |   text?: string; | ||||||
|  |   image_url?: { | ||||||
|  |     url: string; | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface RequestMessage { | export interface RequestMessage { | ||||||
|   role: MessageRole; |   role: MessageRole; | ||||||
|   content: string; |   content: string | MultimodalContent[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface LLMConfig { | export interface LLMConfig { | ||||||
| @@ -143,7 +151,6 @@ export function getHeaders() { | |||||||
|   const accessStore = useAccessStore.getState(); |   const accessStore = useAccessStore.getState(); | ||||||
|   const headers: Record<string, string> = { |   const headers: Record<string, string> = { | ||||||
|     "Content-Type": "application/json", |     "Content-Type": "application/json", | ||||||
|     "x-requested-with": "XMLHttpRequest", |  | ||||||
|     Accept: "application/json", |     Accept: "application/json", | ||||||
|   }; |   }; | ||||||
|   const modelConfig = useChatStore.getState().currentSession().mask.modelConfig; |   const modelConfig = useChatStore.getState().currentSession().mask.modelConfig; | ||||||
|   | |||||||
| @@ -3,6 +3,12 @@ import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api"; | |||||||
| import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | ||||||
| import { getClientConfig } from "@/app/config/client"; | import { getClientConfig } from "@/app/config/client"; | ||||||
| import { DEFAULT_API_HOST } from "@/app/constant"; | import { DEFAULT_API_HOST } from "@/app/constant"; | ||||||
|  | import { | ||||||
|  |   getMessageTextContent, | ||||||
|  |   getMessageImages, | ||||||
|  |   isVisionModel, | ||||||
|  | } from "@/app/utils"; | ||||||
|  |  | ||||||
| export class GeminiProApi implements LLMApi { | export class GeminiProApi implements LLMApi { | ||||||
|   extractMessage(res: any) { |   extractMessage(res: any) { | ||||||
|     console.log("[Response] gemini-pro response: ", res); |     console.log("[Response] gemini-pro response: ", res); | ||||||
| @@ -15,10 +21,33 @@ export class GeminiProApi implements LLMApi { | |||||||
|   } |   } | ||||||
|   async chat(options: ChatOptions): Promise<void> { |   async chat(options: ChatOptions): Promise<void> { | ||||||
|     // const apiClient = this; |     // const apiClient = this; | ||||||
|     const messages = options.messages.map((v) => ({ |     const visionModel = isVisionModel(options.config.model); | ||||||
|  |     let multimodal = false; | ||||||
|  |     const messages = options.messages.map((v) => { | ||||||
|  |       let parts: any[] = [{ text: getMessageTextContent(v) }]; | ||||||
|  |       if (visionModel) { | ||||||
|  |         const images = getMessageImages(v); | ||||||
|  |         if (images.length > 0) { | ||||||
|  |           multimodal = true; | ||||||
|  |           parts = parts.concat( | ||||||
|  |             images.map((image) => { | ||||||
|  |               const imageType = image.split(";")[0].split(":")[1]; | ||||||
|  |               const imageData = image.split(",")[1]; | ||||||
|  |               return { | ||||||
|  |                 inline_data: { | ||||||
|  |                   mime_type: imageType, | ||||||
|  |                   data: imageData, | ||||||
|  |                 }, | ||||||
|  |               }; | ||||||
|  |             }), | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return { | ||||||
|         role: v.role.replace("assistant", "model").replace("system", "user"), |         role: v.role.replace("assistant", "model").replace("system", "user"), | ||||||
|       parts: [{ text: v.content }], |         parts: parts, | ||||||
|     })); |       }; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     // google requires that role in neighboring messages must not be the same |     // google requires that role in neighboring messages must not be the same | ||||||
|     for (let i = 0; i < messages.length - 1; ) { |     for (let i = 0; i < messages.length - 1; ) { | ||||||
| @@ -33,7 +62,9 @@ export class GeminiProApi implements LLMApi { | |||||||
|         i++; |         i++; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     // if (visionModel && messages.length > 1) { | ||||||
|  |     //   options.onError?.(new Error("Multiturn chat is not enabled for models/gemini-pro-vision")); | ||||||
|  |     // } | ||||||
|     const modelConfig = { |     const modelConfig = { | ||||||
|       ...useAppConfig.getState().modelConfig, |       ...useAppConfig.getState().modelConfig, | ||||||
|       ...useChatStore.getState().currentSession().mask.modelConfig, |       ...useChatStore.getState().currentSession().mask.modelConfig, | ||||||
| @@ -80,13 +111,16 @@ export class GeminiProApi implements LLMApi { | |||||||
|     const controller = new AbortController(); |     const controller = new AbortController(); | ||||||
|     options.onController?.(controller); |     options.onController?.(controller); | ||||||
|     try { |     try { | ||||||
|       let chatPath = this.path(Google.ChatPath); |       let googleChatPath = visionModel | ||||||
|  |         ? Google.VisionChatPath | ||||||
|  |         : Google.ChatPath; | ||||||
|  |       let chatPath = this.path(googleChatPath); | ||||||
|  |  | ||||||
|       // let baseUrl = accessStore.googleUrl; |       // let baseUrl = accessStore.googleUrl; | ||||||
|  |  | ||||||
|       if (!baseUrl) { |       if (!baseUrl) { | ||||||
|         baseUrl = isApp |         baseUrl = isApp | ||||||
|           ? DEFAULT_API_HOST + "/api/proxy/google/" + Google.ChatPath |           ? DEFAULT_API_HOST + "/api/proxy/google/" + googleChatPath | ||||||
|           : chatPath; |           : chatPath; | ||||||
|       } |       } | ||||||
|  |  | ||||||
| @@ -152,6 +186,19 @@ export class GeminiProApi implements LLMApi { | |||||||
|               value, |               value, | ||||||
|             }): Promise<any> { |             }): Promise<any> { | ||||||
|               if (done) { |               if (done) { | ||||||
|  |                 if (response.status !== 200) { | ||||||
|  |                   try { | ||||||
|  |                     let data = JSON.parse(ensureProperEnding(partialData)); | ||||||
|  |                     if (data && data[0].error) { | ||||||
|  |                       options.onError?.(new Error(data[0].error.message)); | ||||||
|  |                     } else { | ||||||
|  |                       options.onError?.(new Error("Request failed")); | ||||||
|  |                     } | ||||||
|  |                   } catch (_) { | ||||||
|  |                     options.onError?.(new Error("Request failed")); | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 console.log("Stream complete"); |                 console.log("Stream complete"); | ||||||
|                 // options.onFinish(responseText + remainText); |                 // options.onFinish(responseText + remainText); | ||||||
|                 finished = true; |                 finished = true; | ||||||
|   | |||||||
| @@ -9,7 +9,14 @@ import { | |||||||
| } from "@/app/constant"; | } from "@/app/constant"; | ||||||
| import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; | ||||||
|  |  | ||||||
| import { ChatOptions, getHeaders, LLMApi, LLMModel, LLMUsage } from "../api"; | import { | ||||||
|  |   ChatOptions, | ||||||
|  |   getHeaders, | ||||||
|  |   LLMApi, | ||||||
|  |   LLMModel, | ||||||
|  |   LLMUsage, | ||||||
|  |   MultimodalContent, | ||||||
|  | } from "../api"; | ||||||
| import Locale from "../../locales"; | import Locale from "../../locales"; | ||||||
| import { | import { | ||||||
|   EventStreamContentType, |   EventStreamContentType, | ||||||
| @@ -18,6 +25,11 @@ import { | |||||||
| import { prettyObject } from "@/app/utils/format"; | import { prettyObject } from "@/app/utils/format"; | ||||||
| import { getClientConfig } from "@/app/config/client"; | import { getClientConfig } from "@/app/config/client"; | ||||||
| import { makeAzurePath } from "@/app/azure"; | import { makeAzurePath } from "@/app/azure"; | ||||||
|  | import { | ||||||
|  |   getMessageTextContent, | ||||||
|  |   getMessageImages, | ||||||
|  |   isVisionModel, | ||||||
|  | } from "@/app/utils"; | ||||||
|  |  | ||||||
| export interface OpenAIListModelResponse { | export interface OpenAIListModelResponse { | ||||||
|   object: string; |   object: string; | ||||||
| @@ -72,9 +84,10 @@ export class ChatGPTApi implements LLMApi { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   async chat(options: ChatOptions) { |   async chat(options: ChatOptions) { | ||||||
|  |     const visionModel = isVisionModel(options.config.model); | ||||||
|     const messages = options.messages.map((v) => ({ |     const messages = options.messages.map((v) => ({ | ||||||
|       role: v.role, |       role: v.role, | ||||||
|       content: v.content, |       content: visionModel ? v.content : getMessageTextContent(v), | ||||||
|     })); |     })); | ||||||
|  |  | ||||||
|     const modelConfig = { |     const modelConfig = { | ||||||
|   | |||||||
| @@ -1,5 +1,47 @@ | |||||||
| @import "../styles/animation.scss"; | @import "../styles/animation.scss"; | ||||||
|  |  | ||||||
|  | .attach-images { | ||||||
|  |   position: absolute; | ||||||
|  |   left: 30px; | ||||||
|  |   bottom: 32px; | ||||||
|  |   display: flex; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .attach-image { | ||||||
|  |   cursor: default; | ||||||
|  |   width: 64px; | ||||||
|  |   height: 64px; | ||||||
|  |   border: rgba($color: #888, $alpha: 0.2) 1px solid; | ||||||
|  |   border-radius: 5px; | ||||||
|  |   margin-right: 10px; | ||||||
|  |   background-size: cover; | ||||||
|  |   background-position: center; | ||||||
|  |   background-color: var(--white); | ||||||
|  |  | ||||||
|  |   .attach-image-mask { | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     opacity: 0; | ||||||
|  |     transition: all ease 0.2s; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .attach-image-mask:hover { | ||||||
|  |     opacity: 1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .delete-image { | ||||||
|  |     width: 24px; | ||||||
|  |     height: 24px; | ||||||
|  |     cursor: pointer; | ||||||
|  |     display: flex; | ||||||
|  |     align-items: center; | ||||||
|  |     justify-content: center; | ||||||
|  |     border-radius: 5px; | ||||||
|  |     float: right; | ||||||
|  |     background-color: var(--white); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| .chat-input-actions { | .chat-input-actions { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-wrap: wrap; |   flex-wrap: wrap; | ||||||
| @@ -189,12 +231,10 @@ | |||||||
|  |  | ||||||
|   animation: slide-in ease 0.3s; |   animation: slide-in ease 0.3s; | ||||||
|  |  | ||||||
|   $linear: linear-gradient( |   $linear: linear-gradient(to right, | ||||||
|     to right, |  | ||||||
|       rgba(0, 0, 0, 0), |       rgba(0, 0, 0, 0), | ||||||
|       rgba(0, 0, 0, 1), |       rgba(0, 0, 0, 1), | ||||||
|     rgba(0, 0, 0, 0) |       rgba(0, 0, 0, 0)); | ||||||
|   ); |  | ||||||
|   mask-image: $linear; |   mask-image: $linear; | ||||||
|  |  | ||||||
|   @mixin show { |   @mixin show { | ||||||
| @@ -327,7 +367,7 @@ | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .chat-message-user > .chat-message-container { | .chat-message-user>.chat-message-container { | ||||||
|   align-items: flex-end; |   align-items: flex-end; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -349,6 +389,7 @@ | |||||||
|       padding: 7px; |       padding: 7px; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /* Specific styles for iOS devices */ |   /* Specific styles for iOS devices */ | ||||||
|   @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) { |   @media screen and (max-device-width: 812px) and (-webkit-min-device-pixel-ratio: 2) { | ||||||
|     @supports (-webkit-touch-callout: none) { |     @supports (-webkit-touch-callout: none) { | ||||||
| @@ -381,6 +422,64 @@ | |||||||
|   transition: all ease 0.3s; |   transition: all ease 0.3s; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .chat-message-item-image { | ||||||
|  |   width: 100%; | ||||||
|  |   margin-top: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .chat-message-item-images { | ||||||
|  |   width: 100%; | ||||||
|  |   display: grid; | ||||||
|  |   justify-content: left; | ||||||
|  |   grid-gap: 10px; | ||||||
|  |   grid-template-columns: repeat(var(--image-count), auto); | ||||||
|  |   margin-top: 10px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .chat-message-item-image-multi { | ||||||
|  |   object-fit: cover; | ||||||
|  |   background-size: cover; | ||||||
|  |   background-position: center; | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .chat-message-item-image, | ||||||
|  | .chat-message-item-image-multi { | ||||||
|  |   box-sizing: border-box; | ||||||
|  |   border-radius: 10px; | ||||||
|  |   border: rgba($color: #888, $alpha: 0.2) 1px solid; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @media only screen and (max-width: 600px) { | ||||||
|  |   $calc-image-width: calc(100vw/3*2/var(--image-count)); | ||||||
|  |  | ||||||
|  |   .chat-message-item-image-multi { | ||||||
|  |     width: $calc-image-width; | ||||||
|  |     height: $calc-image-width; | ||||||
|  |   } | ||||||
|  |    | ||||||
|  |   .chat-message-item-image { | ||||||
|  |     max-width: calc(100vw/3*2); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media screen and (min-width: 600px) { | ||||||
|  |   $max-image-width: calc(calc(1200px - var(--sidebar-width))/3*2/var(--image-count)); | ||||||
|  |   $image-width: calc(calc(var(--window-width) - var(--sidebar-width))/3*2/var(--image-count)); | ||||||
|  |  | ||||||
|  |   .chat-message-item-image-multi { | ||||||
|  |     width: $image-width; | ||||||
|  |     height: $image-width; | ||||||
|  |     max-width: $max-image-width; | ||||||
|  |     max-height: $max-image-width; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .chat-message-item-image { | ||||||
|  |     max-width: calc(calc(1200px - var(--sidebar-width))/3*2); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| .chat-message-action-date { | .chat-message-action-date { | ||||||
|   font-size: 12px; |   font-size: 12px; | ||||||
|   opacity: 0.2; |   opacity: 0.2; | ||||||
| @@ -395,7 +494,7 @@ | |||||||
|   z-index: 1; |   z-index: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| .chat-message-user > .chat-message-container > .chat-message-item { | .chat-message-user>.chat-message-container>.chat-message-item { | ||||||
|   background-color: var(--second); |   background-color: var(--second); | ||||||
|  |  | ||||||
|   &:hover { |   &:hover { | ||||||
| @@ -460,6 +559,7 @@ | |||||||
|  |  | ||||||
|       @include single-line(); |       @include single-line(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .hint-content { |     .hint-content { | ||||||
|       font-size: 12px; |       font-size: 12px; | ||||||
|  |  | ||||||
| @@ -474,15 +574,26 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .chat-input-panel-inner { | .chat-input-panel-inner { | ||||||
|  |   cursor: text; | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex: 1; |   flex: 1; | ||||||
|  |   border-radius: 10px; | ||||||
|  |   border: var(--border-in-light); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .chat-input-panel-inner-attach { | ||||||
|  |   padding-bottom: 80px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .chat-input-panel-inner:has(.chat-input:focus) { | ||||||
|  |   border: 1px solid var(--primary); | ||||||
| } | } | ||||||
|  |  | ||||||
| .chat-input { | .chat-input { | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   width: 100%; |   width: 100%; | ||||||
|   border-radius: 10px; |   border-radius: 10px; | ||||||
|   border: var(--border-in-light); |   border: none; | ||||||
|   box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03); |   box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.03); | ||||||
|   background-color: var(--white); |   background-color: var(--white); | ||||||
|   color: var(--black); |   color: var(--black); | ||||||
| @@ -494,9 +605,7 @@ | |||||||
|   min-height: 68px; |   min-height: 68px; | ||||||
| } | } | ||||||
|  |  | ||||||
| .chat-input:focus { | .chat-input:focus {} | ||||||
|   border: 1px solid var(--primary); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .chat-input-send { | .chat-input-send { | ||||||
|   background-color: var(--primary); |   background-color: var(--primary); | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ import ExportIcon from "../icons/share.svg"; | |||||||
| import ReturnIcon from "../icons/return.svg"; | import ReturnIcon from "../icons/return.svg"; | ||||||
| import CopyIcon from "../icons/copy.svg"; | import CopyIcon from "../icons/copy.svg"; | ||||||
| import LoadingIcon from "../icons/three-dots.svg"; | import LoadingIcon from "../icons/three-dots.svg"; | ||||||
|  | import LoadingButtonIcon from "../icons/loading.svg"; | ||||||
| import PromptIcon from "../icons/prompt.svg"; | import PromptIcon from "../icons/prompt.svg"; | ||||||
| import MaskIcon from "../icons/mask.svg"; | import MaskIcon from "../icons/mask.svg"; | ||||||
| import MaxIcon from "../icons/max.svg"; | import MaxIcon from "../icons/max.svg"; | ||||||
| @@ -27,6 +28,7 @@ import PinIcon from "../icons/pin.svg"; | |||||||
| import EditIcon from "../icons/rename.svg"; | import EditIcon from "../icons/rename.svg"; | ||||||
| import ConfirmIcon from "../icons/confirm.svg"; | import ConfirmIcon from "../icons/confirm.svg"; | ||||||
| import CancelIcon from "../icons/cancel.svg"; | import CancelIcon from "../icons/cancel.svg"; | ||||||
|  | import ImageIcon from "../icons/image.svg"; | ||||||
|  |  | ||||||
| import LightIcon from "../icons/light.svg"; | import LightIcon from "../icons/light.svg"; | ||||||
| import DarkIcon from "../icons/dark.svg"; | import DarkIcon from "../icons/dark.svg"; | ||||||
| @@ -53,6 +55,10 @@ import { | |||||||
|   selectOrCopy, |   selectOrCopy, | ||||||
|   autoGrowTextArea, |   autoGrowTextArea, | ||||||
|   useMobileScreen, |   useMobileScreen, | ||||||
|  |   getMessageTextContent, | ||||||
|  |   getMessageImages, | ||||||
|  |   isVisionModel, | ||||||
|  |   compressImage, | ||||||
| } from "../utils"; | } from "../utils"; | ||||||
|  |  | ||||||
| import dynamic from "next/dynamic"; | import dynamic from "next/dynamic"; | ||||||
| @@ -89,6 +95,7 @@ import { prettyObject } from "../utils/format"; | |||||||
| import { ExportMessageModal } from "./exporter"; | import { ExportMessageModal } from "./exporter"; | ||||||
| import { getClientConfig } from "../config/client"; | import { getClientConfig } from "../config/client"; | ||||||
| import { useAllModels } from "../utils/hooks"; | import { useAllModels } from "../utils/hooks"; | ||||||
|  | import { MultimodalContent } from "../client/api"; | ||||||
|  |  | ||||||
| const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { | const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { | ||||||
|   loading: () => <LoadingIcon />, |   loading: () => <LoadingIcon />, | ||||||
| @@ -406,10 +413,14 @@ function useScrollToBottom() { | |||||||
| } | } | ||||||
|  |  | ||||||
| export function ChatActions(props: { | export function ChatActions(props: { | ||||||
|  |   uploadImage: () => void; | ||||||
|  |   setAttachImages: (images: string[]) => void; | ||||||
|  |   setUploading: (uploading: boolean) => void; | ||||||
|   showPromptModal: () => void; |   showPromptModal: () => void; | ||||||
|   scrollToBottom: () => void; |   scrollToBottom: () => void; | ||||||
|   showPromptHints: () => void; |   showPromptHints: () => void; | ||||||
|   hitBottom: boolean; |   hitBottom: boolean; | ||||||
|  |   uploading: boolean; | ||||||
| }) { | }) { | ||||||
|   const config = useAppConfig(); |   const config = useAppConfig(); | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
| @@ -437,8 +448,16 @@ export function ChatActions(props: { | |||||||
|     [allModels], |     [allModels], | ||||||
|   ); |   ); | ||||||
|   const [showModelSelector, setShowModelSelector] = useState(false); |   const [showModelSelector, setShowModelSelector] = useState(false); | ||||||
|  |   const [showUploadImage, setShowUploadImage] = useState(false); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|  |     const show = isVisionModel(currentModel); | ||||||
|  |     setShowUploadImage(show); | ||||||
|  |     if (!show) { | ||||||
|  |       props.setAttachImages([]); | ||||||
|  |       props.setUploading(false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // if current model is not available |     // if current model is not available | ||||||
|     // switch to first available model |     // switch to first available model | ||||||
|     const isUnavaliableModel = !models.some((m) => m.name === currentModel); |     const isUnavaliableModel = !models.some((m) => m.name === currentModel); | ||||||
| @@ -475,6 +494,13 @@ export function ChatActions(props: { | |||||||
|         /> |         /> | ||||||
|       )} |       )} | ||||||
|  |  | ||||||
|  |       {showUploadImage && ( | ||||||
|  |         <ChatAction | ||||||
|  |           onClick={props.uploadImage} | ||||||
|  |           text={Locale.Chat.InputActions.UploadImage} | ||||||
|  |           icon={props.uploading ? <LoadingButtonIcon /> : <ImageIcon />} | ||||||
|  |         /> | ||||||
|  |       )} | ||||||
|       <ChatAction |       <ChatAction | ||||||
|         onClick={nextTheme} |         onClick={nextTheme} | ||||||
|         text={Locale.Chat.InputActions.Theme[theme]} |         text={Locale.Chat.InputActions.Theme[theme]} | ||||||
| @@ -610,6 +636,14 @@ export function EditMessageModal(props: { onClose: () => void }) { | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function DeleteImageButton(props: { deleteImage: () => void }) { | ||||||
|  |   return ( | ||||||
|  |     <div className={styles["delete-image"]} onClick={props.deleteImage}> | ||||||
|  |       <DeleteIcon /> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
| function _Chat() { | function _Chat() { | ||||||
|   type RenderMessage = ChatMessage & { preview?: boolean }; |   type RenderMessage = ChatMessage & { preview?: boolean }; | ||||||
|  |  | ||||||
| @@ -628,6 +662,8 @@ function _Chat() { | |||||||
|   const [hitBottom, setHitBottom] = useState(true); |   const [hitBottom, setHitBottom] = useState(true); | ||||||
|   const isMobileScreen = useMobileScreen(); |   const isMobileScreen = useMobileScreen(); | ||||||
|   const navigate = useNavigate(); |   const navigate = useNavigate(); | ||||||
|  |   const [attachImages, setAttachImages] = useState<string[]>([]); | ||||||
|  |   const [uploading, setUploading] = useState(false); | ||||||
|  |  | ||||||
|   // prompt hints |   // prompt hints | ||||||
|   const promptStore = usePromptStore(); |   const promptStore = usePromptStore(); | ||||||
| @@ -705,7 +741,10 @@ function _Chat() { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     setIsLoading(true); |     setIsLoading(true); | ||||||
|     chatStore.onUserInput(userInput).then(() => setIsLoading(false)); |     chatStore | ||||||
|  |       .onUserInput(userInput, attachImages) | ||||||
|  |       .then(() => setIsLoading(false)); | ||||||
|  |     setAttachImages([]); | ||||||
|     localStorage.setItem(LAST_INPUT_KEY, userInput); |     localStorage.setItem(LAST_INPUT_KEY, userInput); | ||||||
|     setUserInput(""); |     setUserInput(""); | ||||||
|     setPromptHints([]); |     setPromptHints([]); | ||||||
| @@ -783,9 +822,9 @@ function _Chat() { | |||||||
|   }; |   }; | ||||||
|   const onRightClick = (e: any, message: ChatMessage) => { |   const onRightClick = (e: any, message: ChatMessage) => { | ||||||
|     // copy to clipboard |     // copy to clipboard | ||||||
|     if (selectOrCopy(e.currentTarget, message.content)) { |     if (selectOrCopy(e.currentTarget, getMessageTextContent(message))) { | ||||||
|       if (userInput.length === 0) { |       if (userInput.length === 0) { | ||||||
|         setUserInput(message.content); |         setUserInput(getMessageTextContent(message)); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       e.preventDefault(); |       e.preventDefault(); | ||||||
| @@ -853,7 +892,9 @@ function _Chat() { | |||||||
|  |  | ||||||
|     // resend the message |     // resend the message | ||||||
|     setIsLoading(true); |     setIsLoading(true); | ||||||
|     chatStore.onUserInput(userMessage.content).then(() => setIsLoading(false)); |     const textContent = getMessageTextContent(userMessage); | ||||||
|  |     const images = getMessageImages(userMessage); | ||||||
|  |     chatStore.onUserInput(textContent, images).then(() => setIsLoading(false)); | ||||||
|     inputRef.current?.focus(); |     inputRef.current?.focus(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -1048,6 +1089,51 @@ function _Chat() { | |||||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps |     // eslint-disable-next-line react-hooks/exhaustive-deps | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
|  |   async function uploadImage() { | ||||||
|  |     const images: string[] = []; | ||||||
|  |     images.push(...attachImages); | ||||||
|  |  | ||||||
|  |     images.push( | ||||||
|  |       ...(await new Promise<string[]>((res, rej) => { | ||||||
|  |         const fileInput = document.createElement("input"); | ||||||
|  |         fileInput.type = "file"; | ||||||
|  |         fileInput.accept = | ||||||
|  |           "image/png, image/jpeg, image/webp, image/heic, image/heif"; | ||||||
|  |         fileInput.multiple = true; | ||||||
|  |         fileInput.onchange = (event: any) => { | ||||||
|  |           setUploading(true); | ||||||
|  |           const files = event.target.files; | ||||||
|  |           const imagesData: string[] = []; | ||||||
|  |           for (let i = 0; i < files.length; i++) { | ||||||
|  |             const file = event.target.files[i]; | ||||||
|  |             compressImage(file, 256 * 1024) | ||||||
|  |               .then((dataUrl) => { | ||||||
|  |                 imagesData.push(dataUrl); | ||||||
|  |                 if ( | ||||||
|  |                   imagesData.length === 3 || | ||||||
|  |                   imagesData.length === files.length | ||||||
|  |                 ) { | ||||||
|  |                   setUploading(false); | ||||||
|  |                   res(imagesData); | ||||||
|  |                 } | ||||||
|  |               }) | ||||||
|  |               .catch((e) => { | ||||||
|  |                 setUploading(false); | ||||||
|  |                 rej(e); | ||||||
|  |               }); | ||||||
|  |           } | ||||||
|  |         }; | ||||||
|  |         fileInput.click(); | ||||||
|  |       })), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const imagesLength = images.length; | ||||||
|  |     if (imagesLength > 3) { | ||||||
|  |       images.splice(3, imagesLength - 3); | ||||||
|  |     } | ||||||
|  |     setAttachImages(images); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className={styles.chat} key={session.id}> |     <div className={styles.chat} key={session.id}> | ||||||
|       <div className="window-header" data-tauri-drag-region> |       <div className="window-header" data-tauri-drag-region> | ||||||
| @@ -1154,15 +1240,29 @@ function _Chat() { | |||||||
|                           onClick={async () => { |                           onClick={async () => { | ||||||
|                             const newMessage = await showPrompt( |                             const newMessage = await showPrompt( | ||||||
|                               Locale.Chat.Actions.Edit, |                               Locale.Chat.Actions.Edit, | ||||||
|                               message.content, |                               getMessageTextContent(message), | ||||||
|                               10, |                               10, | ||||||
|                             ); |                             ); | ||||||
|  |                             let newContent: string | MultimodalContent[] = | ||||||
|  |                               newMessage; | ||||||
|  |                             const images = getMessageImages(message); | ||||||
|  |                             if (images.length > 0) { | ||||||
|  |                               newContent = [{ type: "text", text: newMessage }]; | ||||||
|  |                               for (let i = 0; i < images.length; i++) { | ||||||
|  |                                 newContent.push({ | ||||||
|  |                                   type: "image_url", | ||||||
|  |                                   image_url: { | ||||||
|  |                                     url: images[i], | ||||||
|  |                                   }, | ||||||
|  |                                 }); | ||||||
|  |                               } | ||||||
|  |                             } | ||||||
|                             chatStore.updateCurrentSession((session) => { |                             chatStore.updateCurrentSession((session) => { | ||||||
|                               const m = session.mask.context |                               const m = session.mask.context | ||||||
|                                 .concat(session.messages) |                                 .concat(session.messages) | ||||||
|                                 .find((m) => m.id === message.id); |                                 .find((m) => m.id === message.id); | ||||||
|                               if (m) { |                               if (m) { | ||||||
|                                 m.content = newMessage; |                                 m.content = newContent; | ||||||
|                               } |                               } | ||||||
|                             }); |                             }); | ||||||
|                           }} |                           }} | ||||||
| @@ -1217,7 +1317,11 @@ function _Chat() { | |||||||
|                               <ChatAction |                               <ChatAction | ||||||
|                                 text={Locale.Chat.Actions.Copy} |                                 text={Locale.Chat.Actions.Copy} | ||||||
|                                 icon={<CopyIcon />} |                                 icon={<CopyIcon />} | ||||||
|                                 onClick={() => copyToClipboard(message.content)} |                                 onClick={() => | ||||||
|  |                                   copyToClipboard( | ||||||
|  |                                     getMessageTextContent(message), | ||||||
|  |                                   ) | ||||||
|  |                                 } | ||||||
|                               /> |                               /> | ||||||
|                             </> |                             </> | ||||||
|                           )} |                           )} | ||||||
| @@ -1232,7 +1336,7 @@ function _Chat() { | |||||||
|                   )} |                   )} | ||||||
|                   <div className={styles["chat-message-item"]}> |                   <div className={styles["chat-message-item"]}> | ||||||
|                     <Markdown |                     <Markdown | ||||||
|                       content={message.content} |                       content={getMessageTextContent(message)} | ||||||
|                       loading={ |                       loading={ | ||||||
|                         (message.preview || message.streaming) && |                         (message.preview || message.streaming) && | ||||||
|                         message.content.length === 0 && |                         message.content.length === 0 && | ||||||
| @@ -1241,12 +1345,42 @@ function _Chat() { | |||||||
|                       onContextMenu={(e) => onRightClick(e, message)} |                       onContextMenu={(e) => onRightClick(e, message)} | ||||||
|                       onDoubleClickCapture={() => { |                       onDoubleClickCapture={() => { | ||||||
|                         if (!isMobileScreen) return; |                         if (!isMobileScreen) return; | ||||||
|                         setUserInput(message.content); |                         setUserInput(getMessageTextContent(message)); | ||||||
|                       }} |                       }} | ||||||
|                       fontSize={fontSize} |                       fontSize={fontSize} | ||||||
|                       parentRef={scrollRef} |                       parentRef={scrollRef} | ||||||
|                       defaultShow={i >= messages.length - 6} |                       defaultShow={i >= messages.length - 6} | ||||||
|                     /> |                     /> | ||||||
|  |                     {getMessageImages(message).length == 1 && ( | ||||||
|  |                       <img | ||||||
|  |                         className={styles["chat-message-item-image"]} | ||||||
|  |                         src={getMessageImages(message)[0]} | ||||||
|  |                         alt="" | ||||||
|  |                       /> | ||||||
|  |                     )} | ||||||
|  |                     {getMessageImages(message).length > 1 && ( | ||||||
|  |                       <div | ||||||
|  |                         className={styles["chat-message-item-images"]} | ||||||
|  |                         style={ | ||||||
|  |                           { | ||||||
|  |                             "--image-count": getMessageImages(message).length, | ||||||
|  |                           } as React.CSSProperties | ||||||
|  |                         } | ||||||
|  |                       > | ||||||
|  |                         {getMessageImages(message).map((image, index) => { | ||||||
|  |                           return ( | ||||||
|  |                             <img | ||||||
|  |                               className={ | ||||||
|  |                                 styles["chat-message-item-image-multi"] | ||||||
|  |                               } | ||||||
|  |                               key={index} | ||||||
|  |                               src={image} | ||||||
|  |                               alt="" | ||||||
|  |                             /> | ||||||
|  |                           ); | ||||||
|  |                         })} | ||||||
|  |                       </div> | ||||||
|  |                     )} | ||||||
|                   </div> |                   </div> | ||||||
|  |  | ||||||
|                   <div className={styles["chat-message-action-date"]}> |                   <div className={styles["chat-message-action-date"]}> | ||||||
| @@ -1266,9 +1400,13 @@ function _Chat() { | |||||||
|         <PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} /> |         <PromptHints prompts={promptHints} onPromptSelect={onPromptSelect} /> | ||||||
|  |  | ||||||
|         <ChatActions |         <ChatActions | ||||||
|  |           uploadImage={uploadImage} | ||||||
|  |           setAttachImages={setAttachImages} | ||||||
|  |           setUploading={setUploading} | ||||||
|           showPromptModal={() => setShowPromptModal(true)} |           showPromptModal={() => setShowPromptModal(true)} | ||||||
|           scrollToBottom={scrollToBottom} |           scrollToBottom={scrollToBottom} | ||||||
|           hitBottom={hitBottom} |           hitBottom={hitBottom} | ||||||
|  |           uploading={uploading} | ||||||
|           showPromptHints={() => { |           showPromptHints={() => { | ||||||
|             // Click again to close |             // Click again to close | ||||||
|             if (promptHints.length > 0) { |             if (promptHints.length > 0) { | ||||||
| @@ -1281,8 +1419,16 @@ function _Chat() { | |||||||
|             onSearch(""); |             onSearch(""); | ||||||
|           }} |           }} | ||||||
|         /> |         /> | ||||||
|         <div className={styles["chat-input-panel-inner"]}> |         <label | ||||||
|  |           className={`${styles["chat-input-panel-inner"]} ${ | ||||||
|  |             attachImages.length != 0 | ||||||
|  |               ? styles["chat-input-panel-inner-attach"] | ||||||
|  |               : "" | ||||||
|  |           }`} | ||||||
|  |           htmlFor="chat-input" | ||||||
|  |         > | ||||||
|           <textarea |           <textarea | ||||||
|  |             id="chat-input" | ||||||
|             ref={inputRef} |             ref={inputRef} | ||||||
|             className={styles["chat-input"]} |             className={styles["chat-input"]} | ||||||
|             placeholder={Locale.Chat.Input(submitKey)} |             placeholder={Locale.Chat.Input(submitKey)} | ||||||
| @@ -1297,6 +1443,29 @@ function _Chat() { | |||||||
|               fontSize: config.fontSize, |               fontSize: config.fontSize, | ||||||
|             }} |             }} | ||||||
|           /> |           /> | ||||||
|  |           {attachImages.length != 0 && ( | ||||||
|  |             <div className={styles["attach-images"]}> | ||||||
|  |               {attachImages.map((image, index) => { | ||||||
|  |                 return ( | ||||||
|  |                   <div | ||||||
|  |                     key={index} | ||||||
|  |                     className={styles["attach-image"]} | ||||||
|  |                     style={{ backgroundImage: `url("${image}")` }} | ||||||
|  |                   > | ||||||
|  |                     <div className={styles["attach-image-mask"]}> | ||||||
|  |                       <DeleteImageButton | ||||||
|  |                         deleteImage={() => { | ||||||
|  |                           setAttachImages( | ||||||
|  |                             attachImages.filter((_, i) => i !== index), | ||||||
|  |                           ); | ||||||
|  |                         }} | ||||||
|  |                       /> | ||||||
|  |                     </div> | ||||||
|  |                   </div> | ||||||
|  |                 ); | ||||||
|  |               })} | ||||||
|  |             </div> | ||||||
|  |           )} | ||||||
|           <IconButton |           <IconButton | ||||||
|             icon={<SendWhiteIcon />} |             icon={<SendWhiteIcon />} | ||||||
|             text={Locale.Chat.Send} |             text={Locale.Chat.Send} | ||||||
| @@ -1304,7 +1473,7 @@ function _Chat() { | |||||||
|             type="primary" |             type="primary" | ||||||
|             onClick={() => doSubmit(userInput)} |             onClick={() => doSubmit(userInput)} | ||||||
|           /> |           /> | ||||||
|         </div> |         </label> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       {showExport && ( |       {showExport && ( | ||||||
|   | |||||||
| @@ -94,6 +94,7 @@ | |||||||
|  |  | ||||||
|   button { |   button { | ||||||
|     flex-grow: 1; |     flex-grow: 1; | ||||||
|  |  | ||||||
|     &:not(:last-child) { |     &:not(:last-child) { | ||||||
|       margin-right: 10px; |       margin-right: 10px; | ||||||
|     } |     } | ||||||
| @@ -190,6 +191,59 @@ | |||||||
|         pre { |         pre { | ||||||
|           overflow: hidden; |           overflow: hidden; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         .message-image { | ||||||
|  |           width: 100%; | ||||||
|  |           margin-top: 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .message-images { | ||||||
|  |           display: grid; | ||||||
|  |           justify-content: left; | ||||||
|  |           grid-gap: 10px; | ||||||
|  |           grid-template-columns: repeat(var(--image-count), auto); | ||||||
|  |           margin-top: 10px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @media screen and (max-width: 600px) { | ||||||
|  |           $image-width: calc(calc(100vw/2)/var(--image-count)); | ||||||
|  |  | ||||||
|  |           .message-image-multi { | ||||||
|  |             width: $image-width; | ||||||
|  |             height: $image-width; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           .message-image { | ||||||
|  |             max-width: calc(100vw/3*2); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         @media screen and (min-width: 600px) { | ||||||
|  |           $max-image-width: calc(900px/3*2/var(--image-count)); | ||||||
|  |           $image-width: calc(80vw/3*2/var(--image-count)); | ||||||
|  |  | ||||||
|  |           .message-image-multi { | ||||||
|  |             width: $image-width; | ||||||
|  |             height: $image-width; | ||||||
|  |             max-width: $max-image-width; | ||||||
|  |             max-height: $max-image-width; | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           .message-image { | ||||||
|  |             max-width: calc(100vw/3*2); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .message-image-multi { | ||||||
|  |           object-fit: cover; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         .message-image, | ||||||
|  |         .message-image-multi { | ||||||
|  |           box-sizing: border-box; | ||||||
|  |           border-radius: 10px; | ||||||
|  |           border: rgba($color: #888, $alpha: 0.2) 1px solid; | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       &-assistant { |       &-assistant { | ||||||
| @@ -213,6 +267,5 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   .default-theme { |   .default-theme {} | ||||||
|   } |  | ||||||
| } | } | ||||||
| @@ -12,7 +12,12 @@ import { | |||||||
|   showToast, |   showToast, | ||||||
| } from "./ui-lib"; | } from "./ui-lib"; | ||||||
| import { IconButton } from "./button"; | import { IconButton } from "./button"; | ||||||
| import { copyToClipboard, downloadAs, useMobileScreen } from "../utils"; | import { | ||||||
|  |   copyToClipboard, | ||||||
|  |   downloadAs, | ||||||
|  |   getMessageImages, | ||||||
|  |   useMobileScreen, | ||||||
|  | } from "../utils"; | ||||||
|  |  | ||||||
| import CopyIcon from "../icons/copy.svg"; | import CopyIcon from "../icons/copy.svg"; | ||||||
| import LoadingIcon from "../icons/three-dots.svg"; | import LoadingIcon from "../icons/three-dots.svg"; | ||||||
| @@ -34,6 +39,7 @@ import { prettyObject } from "../utils/format"; | |||||||
| import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant"; | import { EXPORT_MESSAGE_CLASS_NAME, ModelProvider } from "../constant"; | ||||||
| import { getClientConfig } from "../config/client"; | import { getClientConfig } from "../config/client"; | ||||||
| import { ClientApi } from "../client/api"; | import { ClientApi } from "../client/api"; | ||||||
|  | import { getMessageTextContent } from "../utils"; | ||||||
|  |  | ||||||
| const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { | const Markdown = dynamic(async () => (await import("./markdown")).Markdown, { | ||||||
|   loading: () => <LoadingIcon />, |   loading: () => <LoadingIcon />, | ||||||
| @@ -287,7 +293,7 @@ export function RenderExport(props: { | |||||||
|           id={`${m.role}:${i}`} |           id={`${m.role}:${i}`} | ||||||
|           className={EXPORT_MESSAGE_CLASS_NAME} |           className={EXPORT_MESSAGE_CLASS_NAME} | ||||||
|         > |         > | ||||||
|           <Markdown content={m.content} defaultShow /> |           <Markdown content={getMessageTextContent(m)} defaultShow /> | ||||||
|         </div> |         </div> | ||||||
|       ))} |       ))} | ||||||
|     </div> |     </div> | ||||||
| @@ -580,10 +586,37 @@ export function ImagePreviewer(props: { | |||||||
|  |  | ||||||
|               <div className={styles["body"]}> |               <div className={styles["body"]}> | ||||||
|                 <Markdown |                 <Markdown | ||||||
|                   content={m.content} |                   content={getMessageTextContent(m)} | ||||||
|                   fontSize={config.fontSize} |                   fontSize={config.fontSize} | ||||||
|                   defaultShow |                   defaultShow | ||||||
|                 /> |                 /> | ||||||
|  |                 {getMessageImages(m).length == 1 && ( | ||||||
|  |                   <img | ||||||
|  |                     key={i} | ||||||
|  |                     src={getMessageImages(m)[0]} | ||||||
|  |                     alt="message" | ||||||
|  |                     className={styles["message-image"]} | ||||||
|  |                   /> | ||||||
|  |                 )} | ||||||
|  |                 {getMessageImages(m).length > 1 && ( | ||||||
|  |                   <div | ||||||
|  |                     className={styles["message-images"]} | ||||||
|  |                     style={ | ||||||
|  |                       { | ||||||
|  |                         "--image-count": getMessageImages(m).length, | ||||||
|  |                       } as React.CSSProperties | ||||||
|  |                     } | ||||||
|  |                   > | ||||||
|  |                     {getMessageImages(m).map((src, i) => ( | ||||||
|  |                       <img | ||||||
|  |                         key={i} | ||||||
|  |                         src={src} | ||||||
|  |                         alt="message" | ||||||
|  |                         className={styles["message-image-multi"]} | ||||||
|  |                       /> | ||||||
|  |                     ))} | ||||||
|  |                   </div> | ||||||
|  |                 )} | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           ); |           ); | ||||||
| @@ -602,8 +635,10 @@ export function MarkdownPreviewer(props: { | |||||||
|     props.messages |     props.messages | ||||||
|       .map((m) => { |       .map((m) => { | ||||||
|         return m.role === "user" |         return m.role === "user" | ||||||
|           ? `## ${Locale.Export.MessageFromYou}:\n${m.content}` |           ? `## ${Locale.Export.MessageFromYou}:\n${getMessageTextContent(m)}` | ||||||
|           : `## ${Locale.Export.MessageFromChatGPT}:\n${m.content.trim()}`; |           : `## ${Locale.Export.MessageFromChatGPT}:\n${getMessageTextContent( | ||||||
|  |               m, | ||||||
|  |             ).trim()}`; | ||||||
|       }) |       }) | ||||||
|       .join("\n\n"); |       .join("\n\n"); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ import { | |||||||
|   useAppConfig, |   useAppConfig, | ||||||
|   useChatStore, |   useChatStore, | ||||||
| } from "../store"; | } from "../store"; | ||||||
| import { ROLES } from "../client/api"; | import { MultimodalContent, ROLES } from "../client/api"; | ||||||
| import { | import { | ||||||
|   Input, |   Input, | ||||||
|   List, |   List, | ||||||
| @@ -38,7 +38,12 @@ import { useNavigate } from "react-router-dom"; | |||||||
|  |  | ||||||
| import chatStyle from "./chat.module.scss"; | import chatStyle from "./chat.module.scss"; | ||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { copyToClipboard, downloadAs, readFromFile } from "../utils"; | import { | ||||||
|  |   copyToClipboard, | ||||||
|  |   downloadAs, | ||||||
|  |   getMessageImages, | ||||||
|  |   readFromFile, | ||||||
|  | } from "../utils"; | ||||||
| import { Updater } from "../typing"; | import { Updater } from "../typing"; | ||||||
| import { ModelConfigList } from "./model-config"; | import { ModelConfigList } from "./model-config"; | ||||||
| import { FileName, Path } from "../constant"; | import { FileName, Path } from "../constant"; | ||||||
| @@ -50,6 +55,7 @@ import { | |||||||
|   Draggable, |   Draggable, | ||||||
|   OnDragEndResponder, |   OnDragEndResponder, | ||||||
| } from "@hello-pangea/dnd"; | } from "@hello-pangea/dnd"; | ||||||
|  | import { getMessageTextContent } from "../utils"; | ||||||
|  |  | ||||||
| // drag and drop helper function | // drag and drop helper function | ||||||
| function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] { | function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] { | ||||||
| @@ -244,7 +250,7 @@ function ContextPromptItem(props: { | |||||||
|         </> |         </> | ||||||
|       )} |       )} | ||||||
|       <Input |       <Input | ||||||
|         value={props.prompt.content} |         value={getMessageTextContent(props.prompt)} | ||||||
|         type="text" |         type="text" | ||||||
|         className={chatStyle["context-content"]} |         className={chatStyle["context-content"]} | ||||||
|         rows={focusingInput ? 5 : 1} |         rows={focusingInput ? 5 : 1} | ||||||
| @@ -289,7 +295,18 @@ export function ContextPrompts(props: { | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const updateContextPrompt = (i: number, prompt: ChatMessage) => { |   const updateContextPrompt = (i: number, prompt: ChatMessage) => { | ||||||
|     props.updateContext((context) => (context[i] = prompt)); |     props.updateContext((context) => { | ||||||
|  |       const images = getMessageImages(context[i]); | ||||||
|  |       context[i] = prompt; | ||||||
|  |       if (images.length > 0) { | ||||||
|  |         const text = getMessageTextContent(context[i]); | ||||||
|  |         const newContext: MultimodalContent[] = [{ type: "text", text }]; | ||||||
|  |         for (const img of images) { | ||||||
|  |           newContext.push({ type: "image_url", image_url: { url: img } }); | ||||||
|  |         } | ||||||
|  |         context[i].content = newContext; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const onDragEnd: OnDragEndResponder = (result) => { |   const onDragEnd: OnDragEndResponder = (result) => { | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import { MaskAvatar } from "./mask"; | |||||||
| import Locale from "../locales"; | import Locale from "../locales"; | ||||||
|  |  | ||||||
| import styles from "./message-selector.module.scss"; | import styles from "./message-selector.module.scss"; | ||||||
|  | import { getMessageTextContent } from "../utils"; | ||||||
|  |  | ||||||
| function useShiftRange() { | function useShiftRange() { | ||||||
|   const [startIndex, setStartIndex] = useState<number>(); |   const [startIndex, setStartIndex] = useState<number>(); | ||||||
| @@ -103,7 +104,9 @@ export function MessageSelector(props: { | |||||||
|     const searchResults = new Set<string>(); |     const searchResults = new Set<string>(); | ||||||
|     if (text.length > 0) { |     if (text.length > 0) { | ||||||
|       messages.forEach((m) => |       messages.forEach((m) => | ||||||
|         m.content.includes(text) ? searchResults.add(m.id!) : null, |         getMessageTextContent(m).includes(text) | ||||||
|  |           ? searchResults.add(m.id!) | ||||||
|  |           : null, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     setSearchIds(searchResults); |     setSearchIds(searchResults); | ||||||
| @@ -219,7 +222,7 @@ export function MessageSelector(props: { | |||||||
|                   {new Date(m.date).toLocaleString()} |                   {new Date(m.date).toLocaleString()} | ||||||
|                 </div> |                 </div> | ||||||
|                 <div className={`${styles["content"]} one-line`}> |                 <div className={`${styles["content"]} one-line`}> | ||||||
|                   {m.content} |                   {getMessageTextContent(m)} | ||||||
|                 </div> |                 </div> | ||||||
|               </div> |               </div> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1081,8 +1081,8 @@ export function Settings() { | |||||||
|                         ></input> |                         ></input> | ||||||
|                       </ListItem> |                       </ListItem> | ||||||
|                       <ListItem |                       <ListItem | ||||||
|                         title={Locale.Settings.Access.Azure.ApiKey.Title} |                         title={Locale.Settings.Access.Google.ApiKey.Title} | ||||||
|                         subTitle={Locale.Settings.Access.Azure.ApiKey.SubTitle} |                         subTitle={Locale.Settings.Access.Google.ApiKey.SubTitle} | ||||||
|                       > |                       > | ||||||
|                         <PasswordInput |                         <PasswordInput | ||||||
|                           value={accessStore.googleApiKey} |                           value={accessStore.googleApiKey} | ||||||
| @@ -1099,9 +1099,9 @@ export function Settings() { | |||||||
|                         /> |                         /> | ||||||
|                       </ListItem> |                       </ListItem> | ||||||
|                       <ListItem |                       <ListItem | ||||||
|                         title={Locale.Settings.Access.Google.ApiVerion.Title} |                         title={Locale.Settings.Access.Google.ApiVersion.Title} | ||||||
|                         subTitle={ |                         subTitle={ | ||||||
|                           Locale.Settings.Access.Google.ApiVerion.SubTitle |                           Locale.Settings.Access.Google.ApiVersion.SubTitle | ||||||
|                         } |                         } | ||||||
|                       > |                       > | ||||||
|                         <input |                         <input | ||||||
|   | |||||||
| @@ -88,6 +88,7 @@ export const Azure = { | |||||||
| export const Google = { | export const Google = { | ||||||
|   ExampleEndpoint: "https://generativelanguage.googleapis.com/", |   ExampleEndpoint: "https://generativelanguage.googleapis.com/", | ||||||
|   ChatPath: "v1beta/models/gemini-pro:generateContent", |   ChatPath: "v1beta/models/gemini-pro:generateContent", | ||||||
|  |   VisionChatPath: "v1beta/models/gemini-pro-vision:generateContent", | ||||||
|  |  | ||||||
|   // /api/openai/v1/chat/completions |   // /api/openai/v1/chat/completions | ||||||
| }; | }; | ||||||
| @@ -103,6 +104,7 @@ Latex block: $$e=mc^2$$ | |||||||
| `; | `; | ||||||
|  |  | ||||||
| export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; | export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; | ||||||
|  | export const GEMINI_SUMMARIZE_MODEL = "gemini-pro"; | ||||||
|  |  | ||||||
| export const KnowledgeCutOffDate: Record<string, string> = { | export const KnowledgeCutOffDate: Record<string, string> = { | ||||||
|   default: "2021-09", |   default: "2021-09", | ||||||
| @@ -278,6 +280,15 @@ export const DEFAULT_MODELS = [ | |||||||
|       providerType: "google", |       providerType: "google", | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     name: "gemini-pro-vision", | ||||||
|  |     available: true, | ||||||
|  |     provider: { | ||||||
|  |       id: "google", | ||||||
|  |       providerName: "Google", | ||||||
|  |       providerType: "google", | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
| ] as const; | ] as const; | ||||||
|  |  | ||||||
| export const CHAT_PAGE_SIZE = 15; | export const CHAT_PAGE_SIZE = 15; | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								app/icons/image.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/icons/image.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" height="16" width="16" version="1.1" xml:space="preserve" style=""><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none"/><g class="currentLayer" style=""><title>Layer 1</title><g id="svg_1" class="" fill="#333" fill-opacity="1"><polygon points="2.4690866470336914,2.4690725803375244 4.447190761566162,2.4690725803375244 4.447190761566162,1.6882386207580566 1.6882381439208984,1.6882386207580566 1.6882381439208984,4.44719123840332 2.4690866470336914,4.44719123840332 " id="svg_2" fill="#333" fill-opacity="1"/><polygon points="11.552804470062256,1.6882386207580566 11.552804470062256,2.4690725803375244 13.530910968780518,2.4690725803375244 13.530910968780518,4.44719123840332 14.311760425567627,4.44719123840332 14.311760425567627,1.6882386207580566 " id="svg_3" fill="#333" fill-opacity="1"/><polygon points="13.530910968780518,13.530919075012207 11.552804470062256,13.530919075012207 11.552804470062256,14.311760902404785 14.311760425567627,14.311760902404785 14.311760425567627,11.552801132202148 13.530910968780518,11.552801132202148 " id="svg_4" fill="#333" fill-opacity="1"/><polygon points="2.4690866470336914,11.552801132202148 1.6882381439208984,11.552801132202148 1.6882381439208984,14.311760902404785 4.447190761566162,14.311760902404785 4.447190761566162,13.530919075012207 2.4690866470336914,13.530919075012207 " id="svg_5" fill="#333" fill-opacity="1"/><path d="M8.830417847409231,6.243117030680995 c0.68169614081525,0 1.2363241834494423,-0.5546280426341942 1.2363241834494423,-1.2363241834494423 S9.51214001610201,3.770468663782117 8.830417847409231,3.770468663782117 s-1.2363241834494423,0.5546280426341942 -1.2363241834494423,1.2363241834494423 S8.14872170659398,6.243117030680995 8.830417847409231,6.243117030680995 z" id="svg_6" fill="#333" fill-opacity="1"/><polygon points="3.7704806327819824,12.229532241821289 12.229516506195068,12.229532241821289 12.229516506195068,9.709510803222656 10.70320463180542,8.099010467529297 8.852166652679443,9.175727844238281 6.275332450866699,7.334256172180176 3.7704806327819824,9.977211952209473 " id="svg_7" fill="#333" fill-opacity="1"/></g></g></svg> | ||||||
| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										1
									
								
								app/icons/loading.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								app/icons/loading.svg
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#fff" style=""><rect id="backgroundrect" width="100%" height="100%" x="0" y="0" fill="none" stroke="none" style="" class="" /><g class="currentLayer" style=""><title>Layer 1</title><circle cx="4" cy="8" r="1.926" fill="#333" id="svg_1" class=""><animate attributeName="r" begin="0s" calcMode="linear" dur="0.8s" from="2" repeatCount="indefinite" to="2" values="2;1.2;2" /><animate attributeName="fill-opacity" begin="0s" calcMode="linear" dur="0.8s" from="1" repeatCount="indefinite" to="1" values="1;.5;1" /></circle><circle cx="8" cy="8" r="1.2736" fill="#333" fill-opacity=".3" id="svg_2" class=""><animate attributeName="r" begin="0s" calcMode="linear" dur="0.8s" from="1.2" repeatCount="indefinite" to="1.2" values="1.2;2;1.2" /><animate attributeName="fill-opacity" begin="0s" calcMode="linear" dur="0.8s" from=".5" repeatCount="indefinite" to=".5" values=".5;1;.5" /></circle><circle cx="12" cy="8" r="1.926" fill="#333" id="svg_3" class=""><animate attributeName="r" begin="0s" calcMode="linear" dur="0.8s" from="2" repeatCount="indefinite" to="2" values="2;1.2;2" /><animate attributeName="fill-opacity" begin="0s" calcMode="linear" dur="0.8s" from="1" repeatCount="indefinite" to="1" values="1;.5;1" /></circle></g></svg> | ||||||
| After Width: | Height: | Size: 1.3 KiB | 
| @@ -63,6 +63,7 @@ const cn = { | |||||||
|       Masks: "所有面具", |       Masks: "所有面具", | ||||||
|       Clear: "清除聊天", |       Clear: "清除聊天", | ||||||
|       Settings: "对话设置", |       Settings: "对话设置", | ||||||
|  |       UploadImage: "上传图片", | ||||||
|     }, |     }, | ||||||
|     Rename: "重命名对话", |     Rename: "重命名对话", | ||||||
|     Typing: "正在输入…", |     Typing: "正在输入…", | ||||||
| @@ -314,19 +315,19 @@ const cn = { | |||||||
|       }, |       }, | ||||||
|       Google: { |       Google: { | ||||||
|         ApiKey: { |         ApiKey: { | ||||||
|           Title: "接口密钥", |           Title: "API 密钥", | ||||||
|           SubTitle: "使用自定义 Google AI Studio API Key 绕过密码访问限制", |           SubTitle: "从 Google AI 获取您的 API 密钥", | ||||||
|           Placeholder: "Google AI Studio API Key", |           Placeholder: "输入您的 Google AI Studio API 密钥", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         Endpoint: { |         Endpoint: { | ||||||
|           Title: "接口地址", |           Title: "终端地址", | ||||||
|           SubTitle: "不包含请求路径,样例:", |           SubTitle: "示例:", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         ApiVerion: { |         ApiVersion: { | ||||||
|           Title: "接口版本 (gemini-pro api version)", |           Title: "API 版本(仅适用于 gemini-pro)", | ||||||
|           SubTitle: "选择指定的部分版本", |           SubTitle: "选择一个特定的 API 版本", | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|       CustomModel: { |       CustomModel: { | ||||||
|   | |||||||
| @@ -65,6 +65,7 @@ const en: LocaleType = { | |||||||
|       Masks: "Masks", |       Masks: "Masks", | ||||||
|       Clear: "Clear Context", |       Clear: "Clear Context", | ||||||
|       Settings: "Settings", |       Settings: "Settings", | ||||||
|  |       UploadImage: "Upload Images", | ||||||
|     }, |     }, | ||||||
|     Rename: "Rename Chat", |     Rename: "Rename Chat", | ||||||
|     Typing: "Typing…", |     Typing: "Typing…", | ||||||
| @@ -322,9 +323,8 @@ const en: LocaleType = { | |||||||
|       Google: { |       Google: { | ||||||
|         ApiKey: { |         ApiKey: { | ||||||
|           Title: "API Key", |           Title: "API Key", | ||||||
|           SubTitle: |           SubTitle: "Obtain your API Key from Google AI", | ||||||
|             "Bypass password access restrictions using a custom Google AI Studio API Key", |           Placeholder: "Enter your Google AI Studio API Key", | ||||||
|           Placeholder: "Google AI Studio API Key", |  | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         Endpoint: { |         Endpoint: { | ||||||
| @@ -332,9 +332,9 @@ const en: LocaleType = { | |||||||
|           SubTitle: "Example:", |           SubTitle: "Example:", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         ApiVerion: { |         ApiVersion: { | ||||||
|           Title: "API Version (gemini-pro api version)", |           Title: "API Version (specific to gemini-pro)", | ||||||
|           SubTitle: "Select a specific part version", |           SubTitle: "Select a specific API version", | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -334,7 +334,7 @@ const sk: PartialLocaleType = { | |||||||
|           SubTitle: "Príklad:", |           SubTitle: "Príklad:", | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         ApiVerion: { |         ApiVersion: { | ||||||
|           Title: "Verzia API (gemini-pro verzia API)", |           Title: "Verzia API (gemini-pro verzia API)", | ||||||
|           SubTitle: "Vyberte špecifickú verziu časti", |           SubTitle: "Vyberte špecifickú verziu časti", | ||||||
|         }, |         }, | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { trimTopic } from "../utils"; | import { trimTopic, getMessageTextContent } from "../utils"; | ||||||
|  |  | ||||||
| import Locale, { getLang } from "../locales"; | import Locale, { getLang } from "../locales"; | ||||||
| import { showToast } from "../components/ui-lib"; | import { showToast } from "../components/ui-lib"; | ||||||
| @@ -12,8 +12,9 @@ import { | |||||||
|   ModelProvider, |   ModelProvider, | ||||||
|   StoreKey, |   StoreKey, | ||||||
|   SUMMARIZE_MODEL, |   SUMMARIZE_MODEL, | ||||||
|  |   GEMINI_SUMMARIZE_MODEL, | ||||||
| } from "../constant"; | } from "../constant"; | ||||||
| import { ClientApi, RequestMessage } from "../client/api"; | import { ClientApi, RequestMessage, MultimodalContent } from "../client/api"; | ||||||
| import { ChatControllerPool } from "../client/controller"; | import { ChatControllerPool } from "../client/controller"; | ||||||
| import { prettyObject } from "../utils/format"; | import { prettyObject } from "../utils/format"; | ||||||
| import { estimateTokenLength } from "../utils/token"; | import { estimateTokenLength } from "../utils/token"; | ||||||
| @@ -84,11 +85,20 @@ function createEmptySession(): ChatSession { | |||||||
|  |  | ||||||
| function getSummarizeModel(currentModel: string) { | function getSummarizeModel(currentModel: string) { | ||||||
|   // if it is using gpt-* models, force to use 3.5 to summarize |   // if it is using gpt-* models, force to use 3.5 to summarize | ||||||
|   return currentModel.startsWith("gpt") ? SUMMARIZE_MODEL : currentModel; |   if (currentModel.startsWith("gpt")) { | ||||||
|  |     return SUMMARIZE_MODEL; | ||||||
|  |   } | ||||||
|  |   if (currentModel.startsWith("gemini-pro")) { | ||||||
|  |     return GEMINI_SUMMARIZE_MODEL; | ||||||
|  |   } | ||||||
|  |   return currentModel; | ||||||
| } | } | ||||||
|  |  | ||||||
| function countMessages(msgs: ChatMessage[]) { | function countMessages(msgs: ChatMessage[]) { | ||||||
|   return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0); |   return msgs.reduce( | ||||||
|  |     (pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)), | ||||||
|  |     0, | ||||||
|  |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| function fillTemplateWith(input: string, modelConfig: ModelConfig) { | function fillTemplateWith(input: string, modelConfig: ModelConfig) { | ||||||
| @@ -280,16 +290,36 @@ export const useChatStore = createPersistStore( | |||||||
|         get().summarizeSession(); |         get().summarizeSession(); | ||||||
|       }, |       }, | ||||||
|  |  | ||||||
|       async onUserInput(content: string) { |       async onUserInput(content: string, attachImages?: string[]) { | ||||||
|         const session = get().currentSession(); |         const session = get().currentSession(); | ||||||
|         const modelConfig = session.mask.modelConfig; |         const modelConfig = session.mask.modelConfig; | ||||||
|  |  | ||||||
|         const userContent = fillTemplateWith(content, modelConfig); |         const userContent = fillTemplateWith(content, modelConfig); | ||||||
|         console.log("[User Input] after template: ", userContent); |         console.log("[User Input] after template: ", userContent); | ||||||
|  |  | ||||||
|         const userMessage: ChatMessage = createMessage({ |         let mContent: string | MultimodalContent[] = userContent; | ||||||
|  |  | ||||||
|  |         if (attachImages && attachImages.length > 0) { | ||||||
|  |           mContent = [ | ||||||
|  |             { | ||||||
|  |               type: "text", | ||||||
|  |               text: userContent, | ||||||
|  |             }, | ||||||
|  |           ]; | ||||||
|  |           mContent = mContent.concat( | ||||||
|  |             attachImages.map((url) => { | ||||||
|  |               return { | ||||||
|  |                 type: "image_url", | ||||||
|  |                 image_url: { | ||||||
|  |                   url: url, | ||||||
|  |                 }, | ||||||
|  |               }; | ||||||
|  |             }), | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  |         let userMessage: ChatMessage = createMessage({ | ||||||
|           role: "user", |           role: "user", | ||||||
|           content: userContent, |           content: mContent, | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         const botMessage: ChatMessage = createMessage({ |         const botMessage: ChatMessage = createMessage({ | ||||||
| @@ -307,7 +337,7 @@ export const useChatStore = createPersistStore( | |||||||
|         get().updateCurrentSession((session) => { |         get().updateCurrentSession((session) => { | ||||||
|           const savedUserMessage = { |           const savedUserMessage = { | ||||||
|             ...userMessage, |             ...userMessage, | ||||||
|             content, |             content: mContent, | ||||||
|           }; |           }; | ||||||
|           session.messages = session.messages.concat([ |           session.messages = session.messages.concat([ | ||||||
|             savedUserMessage, |             savedUserMessage, | ||||||
| @@ -461,7 +491,7 @@ export const useChatStore = createPersistStore( | |||||||
|         ) { |         ) { | ||||||
|           const msg = messages[i]; |           const msg = messages[i]; | ||||||
|           if (!msg || msg.isError) continue; |           if (!msg || msg.isError) continue; | ||||||
|           tokenCount += estimateTokenLength(msg.content); |           tokenCount += estimateTokenLength(getMessageTextContent(msg)); | ||||||
|           reversedRecentMessages.push(msg); |           reversedRecentMessages.push(msg); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -91,7 +91,7 @@ export const ModalConfigValidator = { | |||||||
|     return limitNumber(x, -2, 2, 0); |     return limitNumber(x, -2, 2, 0); | ||||||
|   }, |   }, | ||||||
|   temperature(x: number) { |   temperature(x: number) { | ||||||
|     return limitNumber(x, 0, 1, 1); |     return limitNumber(x, 0, 2, 1); | ||||||
|   }, |   }, | ||||||
|   top_p(x: number) { |   top_p(x: number) { | ||||||
|     return limitNumber(x, 0, 1, 1); |     return limitNumber(x, 0, 1, 1); | ||||||
|   | |||||||
							
								
								
									
										94
									
								
								app/utils.ts
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								app/utils.ts
									
									
									
									
									
								
							| @@ -1,12 +1,16 @@ | |||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { showToast } from "./components/ui-lib"; | import { showToast } from "./components/ui-lib"; | ||||||
| import Locale from "./locales"; | import Locale from "./locales"; | ||||||
|  | import { RequestMessage } from "./client/api"; | ||||||
|  | import { DEFAULT_MODELS } from "./constant"; | ||||||
|  |  | ||||||
| export function trimTopic(topic: string) { | export function trimTopic(topic: string) { | ||||||
|   // Fix an issue where double quotes still show in the Indonesian language |   // Fix an issue where double quotes still show in the Indonesian language | ||||||
|   // This will remove the specified punctuation from the end of the string |   // This will remove the specified punctuation from the end of the string | ||||||
|   // and also trim quotes from both the start and end if they exist. |   // and also trim quotes from both the start and end if they exist. | ||||||
|   return topic.replace(/^["“”]+|["“”]+$/g, "").replace(/[,。!?”“"、,.!?]*$/, ""); |   return topic | ||||||
|  |     .replace(/^["“”]+|["“”]+$/g, "") | ||||||
|  |     .replace(/[,。!?”“"、,.!?]*$/, ""); | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function copyToClipboard(text: string) { | export async function copyToClipboard(text: string) { | ||||||
| @@ -40,8 +44,8 @@ export async function downloadAs(text: string, filename: string) { | |||||||
|       defaultPath: `${filename}`, |       defaultPath: `${filename}`, | ||||||
|       filters: [ |       filters: [ | ||||||
|         { |         { | ||||||
|           name: `${filename.split('.').pop()} files`, |           name: `${filename.split(".").pop()} files`, | ||||||
|           extensions: [`${filename.split('.').pop()}`], |           extensions: [`${filename.split(".").pop()}`], | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           name: "All Files", |           name: "All Files", | ||||||
| @@ -54,7 +58,7 @@ export async function downloadAs(text: string, filename: string) { | |||||||
|       try { |       try { | ||||||
|         await window.__TAURI__.fs.writeBinaryFile( |         await window.__TAURI__.fs.writeBinaryFile( | ||||||
|           result, |           result, | ||||||
|           new Uint8Array([...text].map((c) => c.charCodeAt(0))) |           new Uint8Array([...text].map((c) => c.charCodeAt(0))), | ||||||
|         ); |         ); | ||||||
|         showToast(Locale.Download.Success); |         showToast(Locale.Download.Success); | ||||||
|       } catch (error) { |       } catch (error) { | ||||||
| @@ -77,8 +81,51 @@ export async function downloadAs(text: string, filename: string) { | |||||||
|     element.click(); |     element.click(); | ||||||
|  |  | ||||||
|     document.body.removeChild(element); |     document.body.removeChild(element); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function compressImage(file: File, maxSize: number): Promise<string> { | ||||||
|  |   return new Promise((resolve, reject) => { | ||||||
|  |     const reader = new FileReader(); | ||||||
|  |     reader.onload = (readerEvent: any) => { | ||||||
|  |       const image = new Image(); | ||||||
|  |       image.onload = () => { | ||||||
|  |         let canvas = document.createElement("canvas"); | ||||||
|  |         let ctx = canvas.getContext("2d"); | ||||||
|  |         let width = image.width; | ||||||
|  |         let height = image.height; | ||||||
|  |         let quality = 0.9; | ||||||
|  |         let dataUrl; | ||||||
|  |  | ||||||
|  |         do { | ||||||
|  |           canvas.width = width; | ||||||
|  |           canvas.height = height; | ||||||
|  |           ctx?.clearRect(0, 0, canvas.width, canvas.height); | ||||||
|  |           ctx?.drawImage(image, 0, 0, width, height); | ||||||
|  |           dataUrl = canvas.toDataURL("image/jpeg", quality); | ||||||
|  |  | ||||||
|  |           if (dataUrl.length < maxSize) break; | ||||||
|  |  | ||||||
|  |           if (quality > 0.5) { | ||||||
|  |             // Prioritize quality reduction | ||||||
|  |             quality -= 0.1; | ||||||
|  |           } else { | ||||||
|  |             // Then reduce the size | ||||||
|  |             width *= 0.9; | ||||||
|  |             height *= 0.9; | ||||||
|  |           } | ||||||
|  |         } while (dataUrl.length > maxSize); | ||||||
|  |  | ||||||
|  |         resolve(dataUrl); | ||||||
|  |       }; | ||||||
|  |       image.onerror = reject; | ||||||
|  |       image.src = readerEvent.target.result; | ||||||
|  |     }; | ||||||
|  |     reader.onerror = reject; | ||||||
|  |     reader.readAsDataURL(file); | ||||||
|  |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function readFromFile() { | export function readFromFile() { | ||||||
|   return new Promise<string>((res, rej) => { |   return new Promise<string>((res, rej) => { | ||||||
|     const fileInput = document.createElement("input"); |     const fileInput = document.createElement("input"); | ||||||
| @@ -212,8 +259,41 @@ export function getCSSVar(varName: string) { | |||||||
| export function isMacOS(): boolean { | export function isMacOS(): boolean { | ||||||
|   if (typeof window !== "undefined") { |   if (typeof window !== "undefined") { | ||||||
|     let userAgent = window.navigator.userAgent.toLocaleLowerCase(); |     let userAgent = window.navigator.userAgent.toLocaleLowerCase(); | ||||||
|     const macintosh = /iphone|ipad|ipod|macintosh/.test(userAgent) |     const macintosh = /iphone|ipad|ipod|macintosh/.test(userAgent); | ||||||
|     return !!macintosh |     return !!macintosh; | ||||||
|   } |   } | ||||||
|   return false |   return false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getMessageTextContent(message: RequestMessage) { | ||||||
|  |   if (typeof message.content === "string") { | ||||||
|  |     return message.content; | ||||||
|  |   } | ||||||
|  |   for (const c of message.content) { | ||||||
|  |     if (c.type === "text") { | ||||||
|  |       return c.text ?? ""; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return ""; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getMessageImages(message: RequestMessage): string[] { | ||||||
|  |   if (typeof message.content === "string") { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  |   const urls: string[] = []; | ||||||
|  |   for (const c of message.content) { | ||||||
|  |     if (c.type === "image_url") { | ||||||
|  |       urls.push(c.image_url?.url ?? ""); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return urls; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function isVisionModel(model: string) { | ||||||
|  |   return ( | ||||||
|  |     model.startsWith("gpt-4-vision") || | ||||||
|  |     model.startsWith("gemini-pro-vision") || | ||||||
|  |     !DEFAULT_MODELS.find((m) => m.name == model) | ||||||
|  |   ); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ if (mode !== "export") { | |||||||
|  |  | ||||||
|   nextConfig.rewrites = async () => { |   nextConfig.rewrites = async () => { | ||||||
|     const ret = [ |     const ret = [ | ||||||
|       // adjust for previous verison directly using "/api/proxy/" as proxy base route |       // adjust for previous version directly using "/api/proxy/" as proxy base route | ||||||
|       { |       { | ||||||
|         source: "/api/proxy/v1/:path*", |         source: "/api/proxy/v1/:path*", | ||||||
|         destination: "https://api.openai.com/v1/:path*", |         destination: "https://api.openai.com/v1/:path*", | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ | |||||||
|   }, |   }, | ||||||
|   "package": { |   "package": { | ||||||
|     "productName": "NextChat", |     "productName": "NextChat", | ||||||
|     "version": "2.10.3" |     "version": "2.11.2" | ||||||
|   }, |   }, | ||||||
|   "tauri": { |   "tauri": { | ||||||
|     "allowlist": { |     "allowlist": { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user