feat(genmini): enhance (#3849)

This commit is contained in:
Chen Tao 2025-03-25 08:52:43 +08:00 committed by GitHub
parent 6dff8b2725
commit a1568808d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 180 additions and 24 deletions

View File

@ -518,7 +518,9 @@
"upgrade.success.content": "Please restart the application to complete the upgrade",
"upgrade.success.title": "Upgrade successfully",
"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": {
"sidebar.add.title": "Add to sidebar",
@ -1182,4 +1184,4 @@
"visualization": "Visualization"
}
}
}
}

View File

@ -518,7 +518,9 @@
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
"upgrade.success.title": "アップグレードに成功しました",
"warn.notion.exporting": "Notionにエクスポート中です。重複してエクスポートしないでください! ",
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。"
"warning.rate.limit": "送信が頻繁すぎます。{{seconds}} 秒待ってから再試行してください。",
"download.success": "ダウンロード成功",
"download.failed": "ダウンロードに失敗しました"
},
"minapp": {
"sidebar.add.title": "サイドバーに追加",
@ -1183,4 +1185,4 @@
"visualization": "可視化"
}
}
}
}

View File

@ -518,7 +518,9 @@
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
"upgrade.success.title": "Обновление успешно",
"warn.notion.exporting": "Экспортируется в Notion, пожалуйста, не отправляйте повторные запросы!",
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова."
"warning.rate.limit": "Отправка слишком частая, пожалуйста, подождите {{seconds}} секунд, прежде чем попробовать снова.",
"download.success": "Скачивание успешно завершено",
"download.failed": "Ошибка загрузки"
},
"minapp": {
"sidebar.add.title": "Добавить в боковую панель",
@ -1183,4 +1185,4 @@
"visualization": "Визуализация"
}
}
}
}

View File

@ -518,7 +518,9 @@
"upgrade.success.content": "重启用以完成升级",
"upgrade.success.title": "升级成功",
"warn.notion.exporting": "正在导出到 Notion, 请勿重复请求导出!",
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试"
"warning.rate.limit": "发送过于频繁,请等待 {{seconds}} 秒后再尝试",
"download.success": "下载成功",
"download.failed": "下载失败"
},
"minapp": {
"sidebar.add.title": "添加到侧边栏",
@ -1183,4 +1185,4 @@
"visualization": "可视化"
}
}
}
}

View File

@ -518,7 +518,9 @@
"upgrade.success.content": "請重新啟動程式以完成升級",
"upgrade.success.title": "升級成功",
"warn.notion.exporting": "正在匯出到 Notion請勿重複請求匯出",
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試"
"warning.rate.limit": "發送過於頻繁,請在 {{seconds}} 秒後再嘗試",
"download.success": "下載成功",
"download.failed": "下載失敗"
},
"minapp": {
"sidebar.add.title": "新增到側邊欄",
@ -1183,4 +1185,4 @@
"visualization": "視覺化"
}
}
}
}

View File

@ -1,6 +1,17 @@
import {
CopyOutlined,
DownloadOutlined,
RotateLeftOutlined,
RotateRightOutlined,
SwapOutlined,
UndoOutlined,
ZoomInOutlined,
ZoomOutOutlined
} from '@ant-design/icons'
import { Message } from '@renderer/types'
import { Image as AntdImage } from 'antd'
import { Image as AntdImage, Space } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
@ -8,10 +19,88 @@ interface Props {
}
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 (
<Container style={{ marginBottom: 8 }}>
{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>
)
@ -25,5 +114,19 @@ const Container = styled.div`
const Image = styled(AntdImage)`
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

View File

@ -580,7 +580,7 @@ export default class GeminiProvider extends BaseProvider {
private async generateImageExp({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel
const { contextCount } = getAssistantSettings(assistant)
const { contextCount, streamOutput, maxTokens } = getAssistantSettings(assistant)
const userMessages = filterUserRoleStartMessages(filterContextMessages(takeRight(messages, contextCount + 2)))
onFilterMessages(userMessages)
@ -603,16 +603,22 @@ export default class GeminiProvider extends BaseProvider {
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)
if (!isValid) {
throw new Error(`Gemini API error: ${message}`)
this.processGeminiImageResponse(response, onChunk)
return
}
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(
modelId: string,
contents: ContentListUnion
contents: ContentListUnion,
maxTokens?: number
): Promise<GenerateContentResponse> {
try {
return await this.imageSdk.models.generateContent({
@ -650,7 +657,29 @@ export default class GeminiProvider extends BaseProvider {
contents: contents,
config: {
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) {
@ -678,7 +707,9 @@ export default class GeminiProvider extends BaseProvider {
*/
private processGeminiImageResponse(response: any, onChunk: (chunk: ChunkCallbackData) => void): void {
const parts = response.candidates[0].content.parts
if (!parts) {
return
}
// 提取图像数据
const images = parts
.filter((part: Part) => part.inlineData)

View File

@ -15,7 +15,16 @@ interface ChunkCallbackData {
interface CompletionsParams {
messages: Message[]
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
mcpTools?: MCPTool[]
}

View File

@ -127,7 +127,10 @@ export async function fetchChatCompletion({
if (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,
generateImage: generateImage