feat(genmini): enhance (#3849)
This commit is contained in:
parent
6dff8b2725
commit
a1568808d4
@ -518,7 +518,9 @@
|
|||||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||||
"upgrade.success.title": "Upgrade successfully",
|
"upgrade.success.title": "Upgrade successfully",
|
||||||
"warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!",
|
"warn.notion.exporting": "Exporting to Notion, please do not request export repeatedly!",
|
||||||
"warning.rate.limit": "Too many requests. Please wait {{seconds}} seconds before trying again."
|
"warning.rate.limit": "Too many requests. Please wait {{seconds}} seconds before trying again.",
|
||||||
|
"download.success": "Download successful",
|
||||||
|
"download.failed": "Download failed"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "Add to sidebar",
|
"sidebar.add.title": "Add to sidebar",
|
||||||
@ -1182,4 +1184,4 @@
|
|||||||
"visualization": "Visualization"
|
"visualization": "Visualization"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -518,7 +518,9 @@
|
|||||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||||
"upgrade.success.title": "アップグレードに成功しました",
|
"upgrade.success.title": "アップグレードに成功しました",
|
||||||
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
|
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
|
||||||
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。"
|
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。",
|
||||||
|
"download.success": "ダウンロード成功",
|
||||||
|
"download.failed": "ダウンロードに失敗しました"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "サイドバーに追加",
|
"sidebar.add.title": "サイドバーに追加",
|
||||||
@ -1183,4 +1185,4 @@
|
|||||||
"visualization": "可視化"
|
"visualization": "可視化"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -518,7 +518,9 @@
|
|||||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||||
"upgrade.success.title": "Обновление успешно",
|
"upgrade.success.title": "Обновление успешно",
|
||||||
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
|
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
|
||||||
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова."
|
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова.",
|
||||||
|
"download.success": "Скачивание успешно завершено",
|
||||||
|
"download.failed": "Ошибка загрузки"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "Добавить в боковую панель",
|
"sidebar.add.title": "Добавить в боковую панель",
|
||||||
@ -1183,4 +1185,4 @@
|
|||||||
"visualization": "Визуализация"
|
"visualization": "Визуализация"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -518,7 +518,9 @@
|
|||||||
"upgrade.success.content": "重启用以完成升级",
|
"upgrade.success.content": "重启用以完成升级",
|
||||||
"upgrade.success.title": "升级成功",
|
"upgrade.success.title": "升级成功",
|
||||||
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!",
|
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!",
|
||||||
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试"
|
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试",
|
||||||
|
"download.success": "下载成功",
|
||||||
|
"download.failed": "下载失败"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "添加到侧边栏",
|
"sidebar.add.title": "添加到侧边栏",
|
||||||
@ -1183,4 +1185,4 @@
|
|||||||
"visualization": "可视化"
|
"visualization": "可视化"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -518,7 +518,9 @@
|
|||||||
"upgrade.success.content": "請重新啟動程式以完成升級",
|
"upgrade.success.content": "請重新啟動程式以完成升級",
|
||||||
"upgrade.success.title": "升級成功",
|
"upgrade.success.title": "升級成功",
|
||||||
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!",
|
"warn.notion.exporting": "正在匯出到 Notion,請勿重複請求匯出!",
|
||||||
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試"
|
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試",
|
||||||
|
"download.success": "下載成功",
|
||||||
|
"download.failed": "下載失敗"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
"sidebar.add.title": "新增到側邊欄",
|
"sidebar.add.title": "新增到側邊欄",
|
||||||
@ -1183,4 +1185,4 @@
|
|||||||
"visualization": "視覺化"
|
"visualization": "視覺化"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,6 +1,17 @@
|
|||||||
|
import {
|
||||||
|
CopyOutlined,
|
||||||
|
DownloadOutlined,
|
||||||
|
RotateLeftOutlined,
|
||||||
|
RotateRightOutlined,
|
||||||
|
SwapOutlined,
|
||||||
|
UndoOutlined,
|
||||||
|
ZoomInOutlined,
|
||||||
|
ZoomOutOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { Image as AntdImage } from 'antd'
|
import { Image as AntdImage, Space } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -8,10 +19,88 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MessageImage: FC<Props> = ({ message }) => {
|
const MessageImage: FC<Props> = ({ message }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const onDownload = (imageBase64: string, index: number) => {
|
||||||
|
try {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = imageBase64
|
||||||
|
link.download = `image-${Date.now()}-${index}.png`
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.message.success(t('message.download.success'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('下载图片失败:', error)
|
||||||
|
window.message.error(t('message.download.failed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制 base64 图片到剪贴板
|
||||||
|
const onCopy = async (imageBase64: string) => {
|
||||||
|
try {
|
||||||
|
const base64Data = imageBase64.split(',')[1]
|
||||||
|
const mimeType = imageBase64.split(';')[0].split(':')[1]
|
||||||
|
|
||||||
|
const byteCharacters = atob(base64Data)
|
||||||
|
const byteArrays: Uint8Array[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < byteCharacters.length; i += 512) {
|
||||||
|
const slice = byteCharacters.slice(i, i + 512)
|
||||||
|
|
||||||
|
const byteNumbers = new Array(slice.length)
|
||||||
|
for (let j = 0; j < slice.length; j++) {
|
||||||
|
byteNumbers[j] = slice.charCodeAt(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteArray = new Uint8Array(byteNumbers)
|
||||||
|
byteArrays.push(byteArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob(byteArrays, { type: mimeType })
|
||||||
|
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({
|
||||||
|
[blob.type]: blob
|
||||||
|
})
|
||||||
|
])
|
||||||
|
|
||||||
|
window.message.success(t('message.copy.success'))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制图片失败:', error)
|
||||||
|
window.message.error(t('message.copy.failed'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container style={{ marginBottom: 8 }}>
|
<Container style={{ marginBottom: 8 }}>
|
||||||
{message.metadata?.generateImage!.images.map((image, index) => (
|
{message.metadata?.generateImage!.images.map((image, index) => (
|
||||||
<Image src={image} key={`image-${index}`} width="33%" />
|
<Image
|
||||||
|
src={image}
|
||||||
|
key={`image-${index}`}
|
||||||
|
width="33%"
|
||||||
|
preview={{
|
||||||
|
toolbarRender: (
|
||||||
|
_,
|
||||||
|
{
|
||||||
|
transform: { scale },
|
||||||
|
actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset }
|
||||||
|
}
|
||||||
|
) => (
|
||||||
|
<ToobarWrapper size={12} className="toolbar-wrapper">
|
||||||
|
<SwapOutlined rotate={90} onClick={onFlipY} />
|
||||||
|
<SwapOutlined onClick={onFlipX} />
|
||||||
|
<RotateLeftOutlined onClick={onRotateLeft} />
|
||||||
|
<RotateRightOutlined onClick={onRotateRight} />
|
||||||
|
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
|
||||||
|
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
|
||||||
|
<UndoOutlined onClick={onReset} />
|
||||||
|
<CopyOutlined onClick={() => onCopy(image)} />
|
||||||
|
<DownloadOutlined onClick={() => onDownload(image, index)} />
|
||||||
|
</ToobarWrapper>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@ -25,5 +114,19 @@ const Container = styled.div`
|
|||||||
const Image = styled(AntdImage)`
|
const Image = styled(AntdImage)`
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
`
|
`
|
||||||
|
const ToobarWrapper = styled(Space)`
|
||||||
|
padding: 0px 24px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 20px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 100px;
|
||||||
|
.anticon {
|
||||||
|
padding: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.anticon:hover {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export default MessageImage
|
export default MessageImage
|
||||||
|
|||||||
@ -580,7 +580,7 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
private async generateImageExp({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
|
private async generateImageExp({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
|
||||||
const defaultModel = getDefaultModel()
|
const defaultModel = getDefaultModel()
|
||||||
const model = assistant.model || defaultModel
|
const model = assistant.model || defaultModel
|
||||||
const { contextCount } = getAssistantSettings(assistant)
|
const { contextCount, streamOutput, maxTokens } = getAssistantSettings(assistant)
|
||||||
|
|
||||||
const userMessages = filterUserRoleStartMessages(filterContextMessages(takeRight(messages, contextCount + 2)))
|
const userMessages = filterUserRoleStartMessages(filterContextMessages(takeRight(messages, contextCount + 2)))
|
||||||
onFilterMessages(userMessages)
|
onFilterMessages(userMessages)
|
||||||
@ -603,16 +603,22 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
|
|
||||||
contents = await this.addImageFileToContents(userLastMessage, contents)
|
contents = await this.addImageFileToContents(userLastMessage, contents)
|
||||||
|
|
||||||
const response = await this.callGeminiGenerateContent(model.id, contents)
|
if (!streamOutput) {
|
||||||
|
const response = await this.callGeminiGenerateContent(model.id, contents, maxTokens)
|
||||||
|
|
||||||
console.log('response', response)
|
const { isValid, message } = this.isValidGeminiResponse(response)
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error(`Gemini API error: ${message}`)
|
||||||
|
}
|
||||||
|
|
||||||
const { isValid, message } = this.isValidGeminiResponse(response)
|
this.processGeminiImageResponse(response, onChunk)
|
||||||
if (!isValid) {
|
return
|
||||||
throw new Error(`Gemini API error: ${message}`)
|
|
||||||
}
|
}
|
||||||
|
const response = await this.callGeminiGenerateContentStream(model.id, contents, maxTokens)
|
||||||
|
|
||||||
this.processGeminiImageResponse(response, onChunk)
|
for await (const chunk of response) {
|
||||||
|
this.processGeminiImageResponse(chunk, onChunk)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -642,7 +648,8 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
*/
|
*/
|
||||||
private async callGeminiGenerateContent(
|
private async callGeminiGenerateContent(
|
||||||
modelId: string,
|
modelId: string,
|
||||||
contents: ContentListUnion
|
contents: ContentListUnion,
|
||||||
|
maxTokens?: number
|
||||||
): Promise<GenerateContentResponse> {
|
): Promise<GenerateContentResponse> {
|
||||||
try {
|
try {
|
||||||
return await this.imageSdk.models.generateContent({
|
return await this.imageSdk.models.generateContent({
|
||||||
@ -650,7 +657,29 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
contents: contents,
|
contents: contents,
|
||||||
config: {
|
config: {
|
||||||
responseModalities: ['Text', 'Image'],
|
responseModalities: ['Text', 'Image'],
|
||||||
responseMimeType: 'text/plain'
|
responseMimeType: 'text/plain',
|
||||||
|
maxOutputTokens: maxTokens
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Gemini API error:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async callGeminiGenerateContentStream(
|
||||||
|
modelId: string,
|
||||||
|
contents: ContentListUnion,
|
||||||
|
maxTokens?: number
|
||||||
|
): Promise<AsyncGenerator<GenerateContentResponse>> {
|
||||||
|
try {
|
||||||
|
return await this.imageSdk.models.generateContentStream({
|
||||||
|
model: modelId,
|
||||||
|
contents: contents,
|
||||||
|
config: {
|
||||||
|
responseModalities: ['Text', 'Image'],
|
||||||
|
responseMimeType: 'text/plain',
|
||||||
|
maxOutputTokens: maxTokens
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -678,7 +707,9 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
*/
|
*/
|
||||||
private processGeminiImageResponse(response: any, onChunk: (chunk: ChunkCallbackData) => void): void {
|
private processGeminiImageResponse(response: any, onChunk: (chunk: ChunkCallbackData) => void): void {
|
||||||
const parts = response.candidates[0].content.parts
|
const parts = response.candidates[0].content.parts
|
||||||
|
if (!parts) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// 提取图像数据
|
// 提取图像数据
|
||||||
const images = parts
|
const images = parts
|
||||||
.filter((part: Part) => part.inlineData)
|
.filter((part: Part) => part.inlineData)
|
||||||
|
|||||||
11
src/renderer/src/providers/index.d.ts
vendored
11
src/renderer/src/providers/index.d.ts
vendored
@ -15,7 +15,16 @@ interface ChunkCallbackData {
|
|||||||
interface CompletionsParams {
|
interface CompletionsParams {
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
onChunk: ({ text, reasoning_content, usage, metrics, search, citations, mcpToolResponse }: ChunkCallbackData) => void
|
onChunk: ({
|
||||||
|
text,
|
||||||
|
reasoning_content,
|
||||||
|
usage,
|
||||||
|
metrics,
|
||||||
|
search,
|
||||||
|
citations,
|
||||||
|
mcpToolResponse,
|
||||||
|
generateImage
|
||||||
|
}: ChunkCallbackData) => void
|
||||||
onFilterMessages: (messages: Message[]) => void
|
onFilterMessages: (messages: Message[]) => void
|
||||||
mcpTools?: MCPTool[]
|
mcpTools?: MCPTool[]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,7 +127,10 @@ export async function fetchChatCompletion({
|
|||||||
if (mcpToolResponse) {
|
if (mcpToolResponse) {
|
||||||
message.metadata = { ...message.metadata, mcpTools: cloneDeep(mcpToolResponse) }
|
message.metadata = { ...message.metadata, mcpTools: cloneDeep(mcpToolResponse) }
|
||||||
}
|
}
|
||||||
if (generateImage) {
|
if (generateImage && generateImage.images.length > 0) {
|
||||||
|
const existingImages = message.metadata?.generateImage?.images || []
|
||||||
|
generateImage.images = [...existingImages, ...generateImage.images]
|
||||||
|
console.log('generateImage', generateImage)
|
||||||
message.metadata = {
|
message.metadata = {
|
||||||
...message.metadata,
|
...message.metadata,
|
||||||
generateImage: generateImage
|
generateImage: generateImage
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user