Skip to content

feat: add upload image for button, and can preview and remove image by one click.#19

Open
MrXnneHang wants to merge 7 commits intoOpen-LLM-VTuber:mainfrom
XnneHangLab:main
Open

feat: add upload image for button, and can preview and remove image by one click.#19
MrXnneHang wants to merge 7 commits intoOpen-LLM-VTuber:mainfrom
XnneHangLab:main

Conversation

@MrXnneHang
Copy link

@MrXnneHang MrXnneHang commented Feb 6, 2026

似乎原来的上传附件的 button click 事件没写。

这里添加了:

  • click 事件,触发后选择文件,但限制 image type
  • 上传图片的缩略图预览
  • 通过缩略图移除附件
  • 缩略图放大预览
  • 对话历史中的显示缩略图
  • 和对话历史的缩略图放大预览

因为我后端对话历史是不存 image 的,所以:

  • 二次加载对话历史图片是不可见的。
  • 另外似乎 clamp(1vh, calc(110px - 5vh), 1000vh)': 这个只是做了近似,某些分辨率,在某些窗口大小下可能表现不佳。

维护者似乎不在 =-=

但 PR 我先挂在这了。

image image image image

…chat history (#2)

* Move attachment previews above input

* Allow footer to grow for attachments

* Style attachment preview container

* Prevent attachment preview from shrinking

* Move attachment previews to dedicated row and lift footer when attachments present (#3)

* Adjust footer attachment layout

* Refine footer lift and default window size
* Add attachment remove control

* make image larger, rise by 0.1vh

* 采用反向算法
* Add attachment preview modal

* Use dialog for attachment preview
* Show image thumbnails in chat history

* Show text with thumbnails in chat history

* Place image thumbnails above text in history
@gemini-code-assist
Copy link

Summary of Changes

Hello @MrXnneHang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求主要引入了用户界面层面的图片附件功能,极大地增强了用户与应用程序的交互能力。它允许用户在发送消息时附带图片,并提供了直观的预览、管理和在聊天历史中显示这些图片的方式。此外,还对应用程序的窗口管理进行了优化,使其默认尺寸更具响应性。尽管后端目前不支持图片持久化,但前端的实现为未来的全面支持奠定了基础。

Highlights

  • 图片上传与预览功能: 新增了图片上传功能,用户现在可以通过点击附件按钮选择图片文件。上传的图片会以缩略图形式显示在输入框上方,并支持点击放大预览和一键移除。
  • 聊天历史中的图片显示: 聊天历史记录现在能够显示用户发送的图片附件。图片以缩略图形式展示,并同样支持点击放大预览。
  • 动态窗口尺寸调整: 应用程序的主窗口现在会根据主显示器的工作区域动态调整其默认尺寸(宽度为50%,高度为60%),以提供更好的响应式体验。
  • 后端集成与限制: 图片数据作为消息的一部分通过WebSocket发送,但由于后端目前不存储图片,因此在二次加载聊天历史时,图片将不可见。此PR仅处理前端的图片发送和显示逻辑。
  • 国际化支持: 为新的图片附件功能添加了中英文的本地化字符串,包括附件、附件数量、移除附件、预览附件等相关文本。

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/main/window-manager.ts
    • 新增 getDefaultWindowSize 方法,用于根据屏幕工作区域计算默认窗口尺寸。
    • createWindowsetSize 方法中应用动态计算的窗口尺寸,替代固定值。
  • src/renderer/src/components/footer/footer-styles.tsx
    • 修改 container 样式函数,使其接受 hasAttachments 参数。
    • 调整页脚容器的高度为 auto 并添加 minHeight,以适应图片附件区域。
    • 根据 isCollapsedhasAttachments 动态调整 bottom 属性,为附件预览腾出空间。
  • src/renderer/src/components/footer/footer.tsx
    • 引入 ImageBsXDialog 组件,用于图片显示和预览。
    • 更新 MessageInput 组件,添加 onAttachFilesattachedCount 属性。
    • MessageInput 中集成隐藏的 input type="file" 元素,通过附件图标触发,并限制为图片类型。
    • 在页脚中条件渲染图片缩略图区域,支持点击缩略图放大预览和移除附件。
    • 添加 DialogRoot 组件,用于显示放大后的图片预览。
  • src/renderer/src/components/sidebar/chat-history-panel.tsx
    • 引入 useStateImageDialog 组件,用于聊天历史中的图片预览。
    • 修改 validMessages 过滤器,使其包含带有图片的聊天消息。
    • 更新 ChatMessage 渲染逻辑,当消息包含图片时,使用 ChatMessage.CustomContent 显示图片缩略图。
    • 为聊天历史中的图片缩略图添加点击放大预览功能,并使用 DialogRoot 实现。
  • src/renderer/src/context/chat-history-context.tsx
    • 导入 ImagePayload 类型。
    • 更新 appendHumanMessage 函数签名,使其接受可选的 images 数组。
    • 修改 newMessage 对象,包含 images 属性。
  • src/renderer/src/hooks/footer/use-footer.ts
    • useTextInput 钩子中集成 handleAttachFilesattachedImageshandleRemoveAttachment 函数。
  • src/renderer/src/hooks/footer/use-text-input.tsx
    • 引入 useTranslationtoasterImagePayload
    • 新增 attachedImages 状态,用于管理已附加的图片。
    • 实现 readFileAsDataUrl 辅助函数,用于将文件读取为 Data URL。
    • 实现 handleAttachFiles 函数,处理文件选择、图片类型验证和状态更新。
    • 修改 handleSend 函数,使其在发送消息时包含 attachedImages,并在发送后清空附件。
    • 新增 handleRemoveAttachment 函数,用于从附件列表中移除指定图片。
  • src/renderer/src/hooks/utils/use-media-capture.tsx
    • 移除本地 ImageData 接口定义,改用从 types/media.ts 导入的 ImagePayload
    • 更新 captureAllMedia 函数,使其返回 ImagePayload[] 类型。
  • src/renderer/src/layout.tsx
    • 调整页脚布局样式,将高度设置为 auto 并添加 minHeight,以支持动态内容高度。
  • src/renderer/src/locales/en/translation.json
    • 新增 'attachFile', 'attachmentsCount', 'removeAttachment', 'previewAttachment', 'imageMessage', 'previewImage', 'failedReadFile', 'unsupportedFileType' 等翻译键。
  • src/renderer/src/locales/zh/translation.json
    • 新增 '上传文件', '已添加{{count}}个附件', '移除附件', '预览附件', '图片附件', '预览图片', '读取附件{{filename}}失败', '仅支持上传图片文件' 等翻译键。
  • src/renderer/src/services/websocket-service.tsx
    • 导入 ImagePayload 类型。
    • Message 接口中添加可选的 images?: ImagePayload[] 属性。
  • src/renderer/src/types/media.ts
    • 新增文件,定义 ImagePayload 接口,用于标准化图片数据结构,包含 sourcedatamime_type
Activity
  • 此拉取请求解决了与附件上传按钮点击事件缺失相关的问题。
  • 作者指出,由于后端不存储图片,已发送的图片在二次加载聊天历史时将不可见。
  • 作者提到 clamp 函数在某些分辨率和窗口大小下可能表现不佳。
  • 作者表示维护者似乎不在,但仍提交了此PR,表明其积极贡献的态度。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

这个 PR 为应用添加了非常实用的图片上传和预览功能。代码实现结构清晰,涉及的组件和 hooks 划分得很好。我发现了一些可以改进的地方,主要关于 React 的最佳实践和代码可维护性。具体来说,我建议为列表使用稳定的 key、集中管理样式,以及将重复的 UI 提取为可复用组件。总的来说,这是一个很棒的功能实现!

<HStack spacing="2" flexWrap="wrap">
{attachedImages.map((image, index) => (
<Box
key={`${image.data}-${index}`}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在 React 中,使用列表的索引 index 或者不稳定的数据(如这里的 base64 字符串 image.data)作为 key 是一种反模式,尤其是在列表项可以被增删的情况下。这可能会导致渲染问题和性能下降。

建议在上传图片时为每个图片生成一个唯一的客户端 ID,并用它作为 key

你可以在 src/renderer/src/hooks/footer/use-text-input.tsx 中做如下修改:

  1. 更新 attachedImages 的 state 类型,使其包含一个客户端 ID:

    const [attachedImages, setAttachedImages] = useState<(ImagePayload & { clientId: string })[]>([]);
  2. handleAttachFiles 函数中,当图片被读取时,为其生成一个唯一的 ID:

    // ...
    try {
      const dataUrl = await readFileAsDataUrl(file);
      newImages.push({
        clientId: crypto.randomUUID(), // 生成唯一 ID
        source: 'upload',
        data: dataUrl,
        mime_type: file.type || 'image/*',
      });
    } // ...
  3. 然后在这里,你就可以使用这个稳定的 clientId 作为 key

Suggested change
key={`${image.data}-${index}`}
key={image.clientId}

Comment on lines 213 to 233
<IconButton
aria-label={t('footer.removeAttachment')}
icon={<BsX />}
size="xs" // 先用 xs 当基准
w="18px"
h="18px"
minW="18px" // IconButton 默认有 minW,不设会缩不下去
p="0"
fontSize="12px" // 控制图标大小(icon 会吃到 fontSize)
position="absolute"
top="1"
right="1"
borderRadius="full"
bg="blackAlpha.700"
color="whiteAlpha.900"
_hover={{ bg: 'blackAlpha.800' }}
onClick={(event) => {
event.stopPropagation();
handleRemoveAttachment(index);
}}
/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这里的 IconButton 样式是内联定义的,并且包含中文注释。为了更好的代码组织和可维护性,建议将这些样式提取到 src/renderer/src/components/footer/footer-styles.tsx 文件中,并移除代码中的注释。

你可以在 footer-styles.tsxFooterStyles 接口和 footerStyles.footer 对象中添加一个新的样式属性 removeAttachmentButton

// In src/renderer/src/components/footer/footer-styles.tsx
// ...
interface FooterStyles {
  // ...
  removeAttachmentButton: SystemStyleObject
}

export const footerStyles: {
  footer: FooterStyles
  // ...
} = {
  footer: {
    // ...
    removeAttachmentButton: {
      size: "xs",
      w: "18px",
      h: "18px",
      minW: "18px",
      p: "0",
      fontSize: "12px",
      position: "absolute",
      top: "1",
      right: "1",
      borderRadius: "full",
      bg: "blackAlpha.700",
      color: "whiteAlpha.900",
      _hover: { bg: "blackAlpha.800" },
    },
  },
  // ...
}

然后在这里使用它,这样代码会更整洁。

Suggested change
<IconButton
aria-label={t('footer.removeAttachment')}
icon={<BsX />}
size="xs" // 先用 xs 当基准
w="18px"
h="18px"
minW="18px" // IconButton 默认有 minW,不设会缩不下去
p="0"
fontSize="12px" // 控制图标大小(icon 会吃到 fontSize)
position="absolute"
top="1"
right="1"
borderRadius="full"
bg="blackAlpha.700"
color="whiteAlpha.900"
_hover={{ bg: 'blackAlpha.800' }}
onClick={(event) => {
event.stopPropagation();
handleRemoveAttachment(index);
}}
/>
<IconButton
aria-label={t('footer.removeAttachment')}
icon={<BsX />}
{...footerStyles.footer.removeAttachmentButton}
onClick={(event) => {
event.stopPropagation();
handleRemoveAttachment(index);
}}
/>

Comment on lines +262 to +284
<DialogRoot
open={Boolean(previewImage)}
onOpenChange={(details) => {
if (!details.open) {
setPreviewImage(null);
}
}}
>
<DialogContent bg="gray.900" maxW="80vw" w="fit-content">
<DialogCloseTrigger />
<DialogBody p="4">
{previewImage && (
<Image
src={previewImage}
alt={t('footer.previewAttachment')}
maxH="80vh"
maxW="80vw"
objectFit="contain"
/>
)}
</DialogBody>
</DialogContent>
</DialogRoot>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

图片预览的 Dialog 组件在这两个文件中几乎完全一样:

  • src/renderer/src/components/footer/footer.tsx
  • src/renderer/src/components/sidebar/chat-history-panel.tsx

为了遵循 DRY (Don't Repeat Yourself) 原则并提高代码的可维护性,建议将这个 Dialog 提取到一个独立的可复用组件中,例如 ImagePreviewDialog

这个新组件可以接收 open, onOpenChange, 和 imageUrl 作为 props。

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 885675470e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

wsContext.sendMessage({
type: 'text-input',
text: inputText.trim(),
text: trimmedText,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Send non-empty text for image-only messages

handleSend appends a fallback label for image-only input (messageText) but still sends text: trimmedText, which is an empty string when the user submits only attachments. In environments where the backend/history payload does not retain image blobs, this creates empty persisted user turns that disappear on reload because ChatHistoryPanel filters out messages with no content and no images; sending a non-empty marker text with the outbound payload avoids silently dropping those turns from history.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant