Skip to content

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

Closed
MrXnneHang wants to merge 31 commits into
Open-LLM-VTuber:mainfrom
XnneHangLab-Mirror:main
Closed

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

Conversation

@MrXnneHang

@MrXnneHang MrXnneHang commented Feb 6, 2026

Copy link
Copy Markdown

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

这里添加了:

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

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

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

维护者似乎不在 =-=

但 PR 我先挂在这了。

image image image image

Summary by CodeRabbit

  • New Features

    • Image attachments for chat messages with preview and fullscreen modal display
    • Mood/emotion tracking system with visual indicators
    • Advanced character pose and appearance blending
    • Configurable voice input (ASR) toggle
  • Improvements

    • Enhanced mouse-following and drag interaction handling
    • Dynamic window sizing and render scaling
    • Refined WebSocket status interaction and disconnect controls
    • Better footer responsiveness with attachment support

…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
Copy Markdown

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.

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

Copy link
Copy Markdown

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}`}

Copy link
Copy Markdown

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);
}}
/>

Copy link
Copy Markdown

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>

Copy link
Copy Markdown

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。

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

Copy link
Copy Markdown

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,

Copy link
Copy Markdown

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 👍 / 👎.

MrXnneHang and others added 21 commits February 8, 2026 20:50
(MrXnneHang/Open-LLM-VTuber-Web-Dev#1)
…models (#9)

* fix: 修复 Live2D 前端忽略 idleMotionGroupName 配置的问题

* fix: fallback to 'Idle' when idleMotionGroupName is not configured

---------

Co-authored-by: MrXnneHang <XnneHang@gmail.com>
Co-authored-by: xnne-bot <xnne-bot@users.noreply.github.com>
…eMotionGroupName (#10)

Reverts the 'Idle' fallback introduced in c268ea1.
Models without idleMotionGroupName should NOT play idle motion,
otherwise idle motion suppresses auto eye-blink.

Co-authored-by: xnne-bot <xnne-bot@users.noreply.github.com>
* update

* update

---------

Co-authored-by: MrXnneHang <XnneHang@gmail.com>
* update

* 先动眼睛后动头,避免眼睛锁死的假人感

---------

Co-authored-by: MrXnneHang <XnneHang@gmail.com>
Co-authored-by: MrXnneHang <XnneHang@gmail.com>
Co-authored-by: MrXnneHang <XnneHang@gmail.com>
… attention 与 idle bank 控制 (#17)

* update

* update

* update

* suppport mouse attn

* update

* update

* mixer p1

* 嘴部参数只在 listening 的时候才会被改动

* mixer complete

---------

Co-authored-by: MrXnneHang <XnneHang@gmail.com>
…ask turn id (#18)

Co-authored-by: MrXnneHang <XnneHang@gmail.com>
…listening states (#19)

Co-authored-by: MrXnneHang <XnneHang@gmail.com>
…outh_form support (#20)

Co-authored-by: MrXnneHang <XnneHang@gmail.com>
…#22)

* update

* smooth-action

---------

Co-authored-by: MrXnneHang <XnneHang@gmail.com>
Co-authored-by: MrXnneHang <XnneHang@gmail.com>
…nc mouth motion (#24)

* Add Live2D speaking diagnostics

* Preserve idle playback across state switches

* Fix recorded idle continuity during speaking

* Remove temporary Live2D diagnostics

* Smooth Live2D speaking mouth motion

* Tune Live2D mouth response faster

---------

Co-authored-by: MrXnneHang <XnneHang@gmail.com>
Co-authored-by: MrXnneHang <XnneHang@gmail.com>
When set-live2d-appearance message has a non-string expression (null),
call setPersistentAppearance(undefined) to reset to the model's default
appearance state.

Co-authored-by: MrXnneHang <xnnehang@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clear transient expression on __neutral__ sentinel

When the backend sends "__neutral__" as the expression value, call
resetExpression() to clear the transient layer instead of trying to
apply a non-existent expression. This ensures expressions don't linger
when the model outputs neutral/default emotion tags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: add PR template from XnneHangLab

---------

Co-authored-by: MrXnneHang <xnnehang@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: xnne-bot <xnne-bot@users.noreply.github.com>
xnne-bot and others added 4 commits May 26, 2026 17:08
)

The expression catalog was previously parsed only from model3.json's
FileReferences.Expressions. Models that use loose exp3 files (not
declared in model3.json) had an empty catalog, causing defaultEmotion
(watermark hide) and other base patches to silently fail.

Now getExpressionCatalog() prefers the backend-provided
modelInfo.expressionCatalog (which includes all expressions from the
preset, including loose files) and falls back to model3.json parsing.

This fixes the watermark not being hidden on startup for models like
薄巧 whose exp3 files are not declared in model3.json.

Co-authored-by: MrXnneHang <xnnehang@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the backend restarts or the user reconnects, the stored
backgroundUrl in localStorage may point to a different host than the
current baseUrl, causing the background image to fail loading silently.

On receiving the background-files message, check if the stored URL's
origin matches the current baseUrl. If not, rewrite the URL path onto
the correct baseUrl. This ensures the background image loads correctly
after reconnection without requiring user intervention.

Co-authored-by: MrXnneHang <xnnehang@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add asrEnabled to character config context, set from set-model-and-conf
- Block mic toggle with toast warning when ASR is disabled
- Reset aiState to idle on error messages to clear stuck "Thinking..."
- Change micOn from localStorage-persisted to session-only state
  (always starts off, user explicitly enables when needed)

Co-authored-by: MrXnneHang <xnnehang@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add backgroundThrottling: false to prevent rAF throttling on focus loss
- Add disable-renderer-backgrounding and disable-background-timer-throttling flags
- Reduce cursor polling interval from 33ms to 100ms (smoothing handles visual quality)
- Cap DPR at 2 and apply configurable renderScale from backend config
- Fix infinite animateEase loop (now stops when scale converges)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR comprehensively extends Live2D appearance and pose control by introducing a multi-layer expression/idle system, adds image attachments to chat messages with preview support, implements mood score tracking throughout the UI, refactors audio playback with turn-aware completion and improved lip-sync, and expands websocket message contracts to drive all new features from the backend.

Changes

Live2D Expression/Pose, Image Attachments, and Mood Features

Layer / File(s) Summary
Live2D Pose Mixing Foundation
src/renderer/src/live2d/mixer/logical-channels.ts, src/renderer/src/live2d/mixer/live2d-parameter-profile.ts, src/renderer/src/live2d/mixer/pose-mixer.ts
Define canonical logical channel identifiers (head, body, eyes, mouth, gaze), map channels to Live2D parameter IDs with blend modes, and implement Mixer.apply() to blend multiple pose layers with weighted averaging and per-channel preference override.
Live2D Recorded Idle Driver
src/renderer/src/live2d/mixer/recorded-idle-driver.ts
Complete idle animation playback: parse motion3 JSON clips into per-channel keyframes with priority-based channel selection, maintain playback state with weighted random clip selection, support loop/transition blending, time-based output smoothing, and Promise-based clip caching with fetch/parse error handling.
Live2D Appearance/Expression Parameter Layer Controller
src/renderer/src/hooks/canvas/live2d-parameter-layer-controller.ts
Asynchronously resolve expression selectors (name or index) to appearance patches via backend expressionCatalog or model3.json parsing, cache patches per model/expression, apply parameter changes by blend mode (Add/Multiply/Overwrite), manage base persistent and transient ephemeral expression layers with versioning.
Live2D Pose Mixer Controller
src/renderer/src/hooks/canvas/live2d-pose-mixer-controller.ts
Central pose composition across idle, speech, backend, and mouse-attention layers with time-based weight/blend transitions, per-channel masking, parameter profile mapping, frame-by-frame runner installation disabling SDK drag injection, debug globals for browser inspection, and smoothing/sanitization of pose values.
Live2D Model Integration and Mouse Follow
src/renderer/WebSDK/src/lappmodel.ts, src/renderer/WebSDK/src/lapplive2dmanager.ts, src/main/window-manager.ts, src/main/index.ts, src/renderer/src/hooks/canvas/use-live2d-model.ts
Update LAppModel to use dynamic CurrentIdleMotionGroupName and conditionally apply body-angle-Y; extend LAppLive2DManager with drag-input enable/disable; implement mouse-follow pose system using on-screen bounds and smooth interpolation; add pointer-to-model coordinate conversion; centralize drag finalization; add global cursor sync via Electron IPC; update window manager for display-derived sizing and disable background throttling.
Live2D Hook Integration and Component Wiring
src/renderer/src/hooks/canvas/use-live2d-appearance.ts, src/renderer/src/hooks/canvas/use-live2d-expression.ts, src/renderer/src/hooks/canvas/use-live2d-pose-mixer.ts, src/renderer/src/hooks/canvas/use-live2d-resize.ts, src/renderer/src/components/canvas/live2d.tsx
Wire pose mixer, appearance sync, and expression selection via new hooks with enforced stable ordering; refactor expression hook to route through parameter layer controller instead of lappAdapter; add renderScale support to resize hook.
Mood Score Context and UI Integration
src/renderer/src/context/mood-context.tsx, src/renderer/src/App.tsx, src/renderer/src/components/electron/input-subtitle.tsx, src/renderer/src/components/footer/ai-state-indicator.tsx
Add React mood context with provider and hook; integrate moodScore into footer and subtitle components via mood-presentation helper mapping score thresholds to emoji/label pairs; wire websocket mood-update message to set clamped score.
Image Attachment Types and Chat Context
src/renderer/src/types/media.ts, src/renderer/src/context/chat-history-context.tsx, src/renderer/src/hooks/utils/use-media-capture.tsx
Define ImagePayload interface (source, data, mime_type); extend chat-history context appendHumanMessage to accept optional images array; update media-capture hook to use shared ImagePayload type.
Image Attachment Input and Footer UI
src/renderer/src/hooks/footer/use-text-input.tsx, src/renderer/src/hooks/footer/use-footer.ts, src/renderer/src/components/footer/footer.tsx, src/renderer/src/components/footer/footer-styles.tsx
Add file attachment handling in text-input (MAX_ATTACHMENTS, MIME validation, async data-URL reading, removal); extend footer hook and component with attachment state and preview modal; update footer-styles to accept hasAttachments parameter and use dynamic height with minHeight.
Image Display in Chat History
src/renderer/src/components/sidebar/chat-history-panel.tsx, src/renderer/src/components/sidebar/sidebar-styles.tsx
Update chat-history-panel to filter and render messages with images; compute hasImages flag to conditionally blank ChatMessage text; render image thumbnails with click/keyboard preview; add modal image viewer; add pre-wrap whitespace preservation.
Audio System Refactoring with Turn Tracking and Lip-Sync
src/renderer/src/hooks/utils/use-audio-task.ts, src/renderer/src/hooks/utils/use-interrupt.ts, src/renderer/src/utils/audio-manager.ts
Refactor useAudioTask to support managePlaybackCompletion mode with refs preventing stale closures; add turn_id tracking across playback lifecycle; implement tool-status-only bypass of queue; install smoothed RMS-based lip-sync via model._wavFileHandler.update override; add per-audio onStop callback; reset backend synth completion on interrupt.
WebSocket Message Type Expansion
src/renderer/src/services/websocket-service.tsx
Extend AudioPayload with turn_id and tts_error; extend Message with optional images; extend Actions with pose/patch modes, mixer weights, idle bank/state/playback; expand MessageEvent with score, members, ownership, display_text, Live2D model/expression, and idle/mixer configuration.
WebSocket Handler Live2D and Mood Integration
src/renderer/src/services/websocket-handler.tsx
Add mixer/idle normalization helpers; wire mood-update to setMoodScore; handle set-live2d-appearance for persistent appearance; route backend pose/mixer-weights/idle through controllers; track conversation turn_id with gating for audio/synth-complete; drop stale payloads; cache-bust background reconnect.
Configuration Context Updates
src/renderer/src/context/character-config-context.tsx, src/renderer/src/context/live2d-config-context.tsx, src/renderer/src/hooks/utils/use-mic-toggle.ts
Extend character-config with asrEnabled flag; extend live2d-config with expressionCatalog and renderScale on ModelInfo, and persistentAppearance on state; block mic start when asrEnabled is false with warning toast.
WebSocket Context and VAD Updates
src/renderer/src/context/websocket-context.tsx, src/renderer/src/context/vad-context.tsx, src/renderer/src/hooks/canvas/use-ws-status.ts
Add disconnect() method to WebSocketContext; change VAD micOn from persisted to transient useState; extend useWSStatus to return isClickable and handleClick; update WebSocketStatus component to use isClickable for interactivity.
UI Styling and Layout
src/renderer/src/layout.tsx
Update layout.tsx footer to use height: auto with minHeight responsive values instead of fixed height, accommodating variable attachment content.
Localization
src/renderer/src/locales/en/translation.json, src/renderer/src/locales/zh/translation.json
Add English and Chinese translation strings for footer attachment UI, sidebar image labels, and error messages; clean up trailing whitespace.
PR Template and Build Artifacts
.github/PULL_REQUEST_TEMPLATE.md, .gitignore, src/renderer/src/utils/task-queue.ts
Add GitHub PR template with standardized Chinese sections and type checklist; update gitignore for electron.vite.config.*.mjs and .cursorrules; modify TaskQueue.clearQueue() to only clear pending queue without resetting in-flight tasks.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


🐰 Pose layers blend like carrots in a stew,
Idle motions dance with expressions true,
Images attach as mood scores gleam,
A symphony of features—the grand Live2D dream!

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/renderer/src/context/live2d-config-context.tsx (1)

129-132: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reset persistentAppearance on the clear-model path too.

When setModelInfo returns early for missing url, modelInfo is cleared but persistentAppearance is kept from the previous model, which leaves stale cross-model state.

✅ Suggested fix
 const setModelInfo = (info: ModelInfo | undefined) => {
+  setPersistentAppearance(undefined);
   if (!info?.url) {
     setModelInfoState(undefined);
     return;
   }
@@
-  setPersistentAppearance(undefined);
   setModelInfoState({

Also applies to: 138-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/context/live2d-config-context.tsx` around lines 129 - 132,
When setModelInfo finds no info.url and clears modelInfo (the early-return path
in setModelInfo), also reset the persistentAppearance state so old appearance
doesn't leak between models; locate the branch that calls
setModelInfoState(undefined) and add a call to clear the persistentAppearance
(e.g., setPersistentAppearance(undefined) or equivalent). Apply the same change
to the second clear-model path referenced (the other early-return around line
138) so both places that clear modelInfo also reset persistentAppearance.
src/renderer/src/hooks/canvas/use-live2d-model.ts (1)

186-218: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

idleMotionGroupName changes are ignored by the reload check.

You added modelInfo?.idleMotionGroupName to the effect dependencies, but needsUpdate still only compares URL and scale. When just the idle group changes, this effect runs and then exits without calling updateModelConfig(), so the new group is never applied.

Suggested fix
     const currentUrl = modelInfo?.url;
     const sdkScale = (window as any).LAppDefine?.CurrentKScale;
+    const sdkIdleMotionGroupName = (window as any).LAppDefine?.CurrentIdleMotionGroupName ?? 'Idle';
     const modelScale = modelInfo?.kScale !== undefined ? Number(modelInfo.kScale) : undefined;
+    const modelIdleMotionGroupName = modelInfo?.idleMotionGroupName ?? 'Idle';

     if (!currentUrl) {
       mouseFollowEnableAtRef.current = Number.POSITIVE_INFINITY;
       clearMouseAttentionFollow();
       return;
     }

     const needsUpdate = currentUrl &&
                         (currentUrl !== prevModelUrlRef.current ||
-                         (sdkScale !== undefined && modelScale !== undefined && sdkScale !== modelScale));
+                         (sdkScale !== undefined && modelScale !== undefined && sdkScale !== modelScale) ||
+                         sdkIdleMotionGroupName !== modelIdleMotionGroupName);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/hooks/canvas/use-live2d-model.ts` around lines 186 - 218,
The effect's reload check (needsUpdate) only compares URL and scale so changes
to modelInfo.idleMotionGroupName are ignored; add tracking for the previous
idleMotionGroupName (e.g., prevIdleMotionGroupRef similar to prevModelUrlRef)
and include a comparison against modelInfo.idleMotionGroupName in the
needsUpdate condition so the effect runs when the idle group changes, then
ensure updateModelConfig is called with the new idleMotionGroupName; locate the
logic around needsUpdate, prevModelUrlRef, parseModelUrl, updateModelConfig,
clearMouseAttentionFollow, and initializeLive2D to implement this change.
🧹 Nitpick comments (2)
src/renderer/src/components/footer/footer-styles.tsx (1)

29-31: ⚡ Quick win

Replace the clamp-based bottom offset with layout-driven spacing.

The viewport-derived bottom shift is brittle and can misplace the footer across resolutions. Prefer keeping bottom: 0 and adjusting spacing via normal layout (padding/margin/content height) instead of visual offsetting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/footer/footer-styles.tsx` around lines 29 - 31,
The clamp-based bottom offset in footer-styles.tsx (the bottom property
currently conditional on isCollapsed and hasAttachments) is brittle; change the
bottom assignment to always be '0px' instead of using clamp, remove the
conditional expression that references isCollapsed and hasAttachments for
bottom, and instead adjust spacing via layout (e.g., add padding/margin to the
footer container or the attachments container) so visual spacing is handled by
normal flow rather than viewport-derived offsetting.
src/renderer/src/components/canvas/ws-status.tsx (1)

29-33: ⚡ Quick win

Conditionally bind onClick to match isClickable.

onClick is always attached even when the status is not clickable; binding it conditionally keeps behavior consistent with the visual state.

💡 Suggested change
-      onClick={handleClick}
+      onClick={isClickable ? handleClick : undefined}
       cursor={isClickable ? 'pointer' : 'default'}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/canvas/ws-status.tsx` around lines 29 - 33, The
onClick handler is always attached even when isClickable is false; update the
JSX to only bind onClick when isClickable is true (e.g., set onClick to
handleClick only when isClickable, otherwise undefined/null) so the interactive
behavior matches the visual state controlled by cursor and _hover; target the
onClick prop that currently uses handleClick and use isClickable to
conditionally attach it.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/PULL_REQUEST_TEMPLATE.md:
- Line 7: Replace the misspelled phrase "featrue request" with the correct
"feature request" in the pull request template text (update the string "featrue
request" to "feature request").

In `@src/renderer/src/components/electron/input-subtitle.tsx`:
- Around line 21-32: getMoodPresentation currently returns hard-coded English
emoji labels and the separate moodDescription strings (around moodDescription at
lines ~54-55) are also hard-coded; update both to use the app's localization
utility (e.g., call the i18n/t translation function or useTranslation hook)
instead of literal strings so labels like 'excited', 'normal', 'low', 'silent'
and the moodDescription text are translated. Locate getMoodPresentation and
replace its label values with translation keys (or translated strings from
t(...)), and modify where moodDescription is defined to call the same
translation function with an appropriate key; keep the emoji characters as-is
and only localize human-readable text/ARIA labels.

In `@src/renderer/src/components/footer/ai-state-indicator.tsx`:
- Around line 26-40: The tooltip/ARIA text is built from the hardcoded string
moodDescription, so localize it using the existing translation function (t)
instead of embedding English; create or use a translation key (e.g.
aiState.moodDescription) and pass mood.label and moodScore as interpolation
params, then replace moodDescription with the localized string for the Box title
and the Text aria-label (references: moodDescription variable, aiState usage,
and the title/aria-label props in this component).

In `@src/renderer/src/components/footer/footer.tsx`:
- Line 111: The aria-label on the attach button is hardcoded as "Attach file";
replace it with the localized string by using the project's i18n function (e.g.,
the useTranslation hook and its t(...) call or i18n.t(...)) inside the Footer
component/Attach button JSX so the screen-reader label uses a translation key
(e.g., "attach_file" or the existing keyspace) instead of a literal string;
update the JSX attribute aria-label={t('your_translation_key_here')} and ensure
useTranslation is imported and initialized in footer.tsx.
- Line 214: The React list key currently uses the entire base64 data URL
(key={`${image.data}-${index}`}) which is large; change the key in the footer
image list rendering to use a stable, small identifier instead — e.g., use a
provided unique id on the image object (image.id) or fallback to the index
(index) or a short deterministic hash/slice of image.data (first N chars) so the
key is compact and stable; update the map/render code in the footer component
where image.data and index are referenced (the expression producing the key) to
use one of these alternatives.

In `@src/renderer/src/hooks/canvas/live2d-parameter-layer-controller.ts`:
- Around line 116-137: The loader Promise stored in expressionCatalogCache for a
given modelUrl can reject and remain cached, causing subsequent calls to reuse a
failed Promise; modify the code that sets expressionCatalogCache.set(modelUrl,
loader) so that if loader rejects it removes the cache entry (e.g., attach a
.catch handler on the loader Promise to call
expressionCatalogCache.delete(modelUrl) and rethrow the error), ensuring
functions like loader, fetchJson, resolveAssetUrl and usage of
modelInfo/modelUrl clear the cache on failure.

In `@src/renderer/src/hooks/canvas/use-live2d-appearance.ts`:
- Around line 14-38: The effect that installs the appearance runner currently
only depends on modelInfo?.url, so when the Live2D model instance is recreated
(as done in use-live2d-model.ts) the effect doesn't re-run and the new model
never gets a _parameterLayerController; update the dependency array to include
the actual Live2D model instance (the value returned by your use-live2d-model
hook, e.g., live2DModel or modelInstance) so the effect re-runs and calls
getLive2DParameterLayerController().installRunner(lappAdapter) for the new
model; keep the existing ensureRunnerInstalled/installRunner logic and
cancelation behavior.

In `@src/renderer/src/hooks/canvas/use-live2d-pose-mixer.ts`:
- Around line 10-37: The effect only re-runs on modelUrl changes so
installRunner never gets retried when the Live2D instance/adapter is recreated
on the same URL; change the useEffect to also capture and depend on the current
LApp adapter identity (e.g. const initialLapp = (window as
any).getLAppAdapter?.()) and include that in the dependency array so
getLive2DPoseMixerController().installRunner(initialLapp) is retried when the
adapter/model instance changes; keep the existing ensureRunnerInstalled logic
and use initialLapp inside it (referencing getLive2DPoseMixerController,
installRunner, ensureRunnerInstalled, modelUrl, and useEffect).

In `@src/renderer/src/hooks/canvas/use-live2d-resize.ts`:
- Around line 202-204: In use-live2d-resize.ts the code reads
modelInfo?.renderScale directly; validate and clamp renderScale before computing
dpr: retrieve modelInfo?.renderScale into a local variable, coerce to a finite
number (fallback to 1 if NaN or non-finite or <= 0), then clamp it to a
reasonable range (e.g., min 0.1, max 4) before using it to compute dpr and set
canvas.width/height (affects variables renderScale, dpr, and canvas.width).

In `@src/renderer/src/hooks/footer/use-text-input.tsx`:
- Around line 94-101: The pre-check in handleSend is returning before calling
captureAllMedia, preventing camera/screen-only sends; move the media capture
before the empty-text/attachment guard or change the guard to consider await
captureAllMedia() — specifically, call captureAllMedia() (referencing the
captureAllMedia function) and merge its result with attachedImages into images
before the early return, then use images.length and trimmed inputText to decide
whether to return; update references to inputText, attachedImages, images, and
messageText accordingly so captures without text are allowed.

In `@src/renderer/src/hooks/utils/use-audio-task.ts`:
- Around line 129-131: The hook is only updating currentTurnIdRef when
displayText exists, causing audio-only events to miss the correct turn_id;
update currentTurnIdRef whenever a turnId is present independent of displayText
(either move the currentTurnIdRef.current = turnId assignment out of the
displayText conditional in useAudioTask or add a separate effect that watches
turnId and sets currentTurnIdRef), so later emission of
"frontend-playback-complete" uses the correct turn id.

In `@src/renderer/src/hooks/utils/use-mic-toggle.ts`:
- Around line 19-23: Replace the hardcoded ASR warning string in
use-mic-toggle.ts with an i18n lookup: import or access the project's
translation function (e.g., useI18n or t) in the same hook, replace the
toaster.create title with t('toast.asrDisabled') (or a similar descriptive key),
and add that key/value to the locales (e.g., en.json, zh.json) so switching
locales changes the toast text; ensure the key is used consistently where
toaster.create is called in the hook.

In `@src/renderer/src/live2d/mixer/recorded-idle-driver.ts`:
- Around line 153-229: parseCurveKeyframes currently only emits endpoint
times/values which flattens Bezier/stepped/inverse-stepped semantics; update
parseCurveKeyframes to record each segment's type and its parameters (for
linear: end time/value; for bezier: c1t/c1v/c2t/c2v/endT/endV; for
stepped/inverse: time/value and type) into the returned data structure (augment
MotionKeyframe or introduce a MotionSegment shape) instead of only endpoints,
and then update sampleCurveValueAtTime to consume those segment records and
perform the proper interpolation per normalizeMotionSegmentType (linear
interpolation for type 0, cubic Bezier evaluation using the control points for
type 1, and stepped/inverse-stepped behavior for types 2/3) so playback matches
authored Motion3 semantics.
- Around line 917-940: loadClip currently stores the in-flight promise into
clipCache immediately, but if the fetch/parse fails the stored promise resolves
to null and poisons the cache; change loadClip (and the local loader promise
handling) so that you either (a) remove/evict the cache entry when the loader
resolves to null or throws, or (b) only set clipCache.set(resolvedUrl, parsed)
after a successful parse (and don't cache null). Specifically update
loadClip/loader to catch failures and call this.clipCache.delete(resolvedUrl)
(or avoid setting the cache until parsed is available) so subsequent calls will
retry instead of returning a cached null.

In `@src/renderer/src/locales/zh/translation.json`:
- Around line 125-131: The zh locale is missing the error key
error.ttsGenerationFailed that exists in en; open
src/renderer/src/locales/zh/translation.json and add an entry for
"error.ttsGenerationFailed" with an appropriate Chinese translation (e.g. a
short message indicating TTS generation failed) alongside the other error keys
(near failedReadFile, failedParseWebSocket, etc.) so the key names match exactly
with the en locale.

In `@src/renderer/src/services/websocket-handler.tsx`:
- Around line 529-531: The current check lets a backend synth-complete fire when
there's no active local turn; tighten it by requiring a matching turn_id: in the
websocket handler change the condition so setBackendSynthComplete(true) is only
called when message.turn_id is truthy AND currentTurnIdRef.current is truthy AND
message.turn_id === currentTurnIdRef.current; use the existing identifiers
(message.turn_id, currentTurnIdRef.current, setBackendSynthComplete) and
ignore/skip completions when the local turn ID is absent or mismatched.

In `@src/renderer/src/services/websocket-service.tsx`:
- Around line 104-121: The MessageEvent interface in websocket-service.tsx is
missing the asr_enabled property which is later accessed as message.asr_enabled
in websocket-handler.tsx; update the MessageEvent type (the interface that
contains score/members/is_owner/.../browser_view) to include asr_enabled?:
boolean (or appropriate type) so consumers like websocket-handler.tsx can safely
read message.asr_enabled without TypeScript errors.

In `@src/renderer/WebSDK/src/lappdefine.ts`:
- Line 39: The updateModelConfig flow is turning an omitted idleMotionGroupName
into null which breaks the default Idle behavior because lappmodel.ts only
starts idle when CurrentIdleMotionGroupName is truthy; update the logic so
CurrentIdleMotionGroupName retains the 'Idle' default when no override is
provided: either (a) initialize/export CurrentIdleMotionGroupName as 'Idle' and
ensure updateModelConfig does not overwrite it with null when
idleMotionGroupName is undefined, or (b) if updateModelConfig must assign, treat
undefined as 'Idle' instead of null; refer to CurrentIdleMotionGroupName and
updateModelConfig (and the checks in lappmodel.ts) when making the change.

---

Outside diff comments:
In `@src/renderer/src/context/live2d-config-context.tsx`:
- Around line 129-132: When setModelInfo finds no info.url and clears modelInfo
(the early-return path in setModelInfo), also reset the persistentAppearance
state so old appearance doesn't leak between models; locate the branch that
calls setModelInfoState(undefined) and add a call to clear the
persistentAppearance (e.g., setPersistentAppearance(undefined) or equivalent).
Apply the same change to the second clear-model path referenced (the other
early-return around line 138) so both places that clear modelInfo also reset
persistentAppearance.

In `@src/renderer/src/hooks/canvas/use-live2d-model.ts`:
- Around line 186-218: The effect's reload check (needsUpdate) only compares URL
and scale so changes to modelInfo.idleMotionGroupName are ignored; add tracking
for the previous idleMotionGroupName (e.g., prevIdleMotionGroupRef similar to
prevModelUrlRef) and include a comparison against modelInfo.idleMotionGroupName
in the needsUpdate condition so the effect runs when the idle group changes,
then ensure updateModelConfig is called with the new idleMotionGroupName; locate
the logic around needsUpdate, prevModelUrlRef, parseModelUrl, updateModelConfig,
clearMouseAttentionFollow, and initializeLive2D to implement this change.

---

Nitpick comments:
In `@src/renderer/src/components/canvas/ws-status.tsx`:
- Around line 29-33: The onClick handler is always attached even when
isClickable is false; update the JSX to only bind onClick when isClickable is
true (e.g., set onClick to handleClick only when isClickable, otherwise
undefined/null) so the interactive behavior matches the visual state controlled
by cursor and _hover; target the onClick prop that currently uses handleClick
and use isClickable to conditionally attach it.

In `@src/renderer/src/components/footer/footer-styles.tsx`:
- Around line 29-31: The clamp-based bottom offset in footer-styles.tsx (the
bottom property currently conditional on isCollapsed and hasAttachments) is
brittle; change the bottom assignment to always be '0px' instead of using clamp,
remove the conditional expression that references isCollapsed and hasAttachments
for bottom, and instead adjust spacing via layout (e.g., add padding/margin to
the footer container or the attachments container) so visual spacing is handled
by normal flow rather than viewport-derived offsetting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 765f7b3f-7956-40ed-8a73-c10dc0618cf4

📥 Commits

Reviewing files that changed from the base of the PR and between d176e7d and 0d14d93.

📒 Files selected for processing (48)
  • .github/PULL_REQUEST_TEMPLATE.md
  • .gitignore
  • src/main/index.ts
  • src/main/window-manager.ts
  • src/renderer/WebSDK/src/lappdefine.ts
  • src/renderer/WebSDK/src/lapplive2dmanager.ts
  • src/renderer/WebSDK/src/lappmodel.ts
  • src/renderer/src/App.tsx
  • src/renderer/src/components/canvas/live2d.tsx
  • src/renderer/src/components/canvas/ws-status.tsx
  • src/renderer/src/components/electron/input-subtitle.tsx
  • src/renderer/src/components/footer/ai-state-indicator.tsx
  • src/renderer/src/components/footer/footer-styles.tsx
  • src/renderer/src/components/footer/footer.tsx
  • src/renderer/src/components/sidebar/chat-history-panel.tsx
  • src/renderer/src/components/sidebar/sidebar-styles.tsx
  • src/renderer/src/context/character-config-context.tsx
  • src/renderer/src/context/chat-history-context.tsx
  • src/renderer/src/context/live2d-config-context.tsx
  • src/renderer/src/context/mood-context.tsx
  • src/renderer/src/context/vad-context.tsx
  • src/renderer/src/context/websocket-context.tsx
  • src/renderer/src/hooks/canvas/live2d-parameter-layer-controller.ts
  • src/renderer/src/hooks/canvas/live2d-pose-mixer-controller.ts
  • src/renderer/src/hooks/canvas/use-live2d-appearance.ts
  • src/renderer/src/hooks/canvas/use-live2d-expression.ts
  • src/renderer/src/hooks/canvas/use-live2d-model.ts
  • src/renderer/src/hooks/canvas/use-live2d-pose-mixer.ts
  • src/renderer/src/hooks/canvas/use-live2d-resize.ts
  • src/renderer/src/hooks/canvas/use-ws-status.ts
  • src/renderer/src/hooks/footer/use-footer.ts
  • src/renderer/src/hooks/footer/use-text-input.tsx
  • src/renderer/src/hooks/utils/use-audio-task.ts
  • src/renderer/src/hooks/utils/use-interrupt.ts
  • src/renderer/src/hooks/utils/use-media-capture.tsx
  • src/renderer/src/hooks/utils/use-mic-toggle.ts
  • src/renderer/src/layout.tsx
  • src/renderer/src/live2d/mixer/live2d-parameter-profile.ts
  • src/renderer/src/live2d/mixer/logical-channels.ts
  • src/renderer/src/live2d/mixer/pose-mixer.ts
  • src/renderer/src/live2d/mixer/recorded-idle-driver.ts
  • src/renderer/src/locales/en/translation.json
  • src/renderer/src/locales/zh/translation.json
  • src/renderer/src/services/websocket-handler.tsx
  • src/renderer/src/services/websocket-service.tsx
  • src/renderer/src/types/media.ts
  • src/renderer/src/utils/audio-manager.ts
  • src/renderer/src/utils/task-queue.ts
💤 Files with no reviewable changes (1)
  • src/renderer/src/utils/task-queue.ts

<!-- 感谢你的贡献 -->
<!-- 为了让我们更快地了解你所作的更改, **请不要删除本模板** -->
<!-- 在开始一个 PR 之前,请确定你已经阅读过 CONTRIBUTING.md -->
<!-- 为了节省你的时间,如果你需要一个新特性,在你开始为其工作之前,最佳选择是开启一个 featrue request 的 issue 让大家一起讨论 -->

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix typo in contributor guidance ("featrue request").

Please change featrue request to feature request to keep the template professional and clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/PULL_REQUEST_TEMPLATE.md at line 7, Replace the misspelled phrase
"featrue request" with the correct "feature request" in the pull request
template text (update the string "featrue request" to "feature request").

Comment on lines +21 to +32
function getMoodPresentation(score: number) {
if (score >= 90) {
return { emoji: '😊', label: 'excited' };
}
if (score >= 80) {
return { emoji: '🙂', label: 'normal' };
}
if (score >= 60) {
return { emoji: '😔', label: 'low' };
}
return { emoji: '😶', label: 'silent' };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the new mood labels and description text.

getMoodPresentation labels and moodDescription are hardcoded English strings, which will produce mixed-language UI/accessibility text when running in other locales.

💡 Suggested change
+import { useTranslation } from 'react-i18next';
@@
 function getMoodPresentation(score: number) {
   if (score >= 90) {
-    return { emoji: '😊', label: 'excited' };
+    return { emoji: '😊', labelKey: 'mood.excited' };
   }
   if (score >= 80) {
-    return { emoji: '🙂', label: 'normal' };
+    return { emoji: '🙂', labelKey: 'mood.normal' };
   }
   if (score >= 60) {
-    return { emoji: '😔', label: 'low' };
+    return { emoji: '😔', labelKey: 'mood.low' };
   }
-  return { emoji: '😶', label: 'silent' };
+  return { emoji: '😶', labelKey: 'mood.silent' };
 }
@@
 export function InputSubtitle() {
+  const { t } = useTranslation();
@@
   const mood = getMoodPresentation(moodScore);
-  const moodDescription = `Mood: ${mood.label} (${moodScore})`;
+  const moodDescription = t('mood.description', {
+    mood: t(mood.labelKey),
+    score: moodScore,
+  });

Also applies to: 54-55

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/electron/input-subtitle.tsx` around lines 21 -
32, getMoodPresentation currently returns hard-coded English emoji labels and
the separate moodDescription strings (around moodDescription at lines ~54-55)
are also hard-coded; update both to use the app's localization utility (e.g.,
call the i18n/t translation function or useTranslation hook) instead of literal
strings so labels like 'excited', 'normal', 'low', 'silent' and the
moodDescription text are translated. Locate getMoodPresentation and replace its
label values with translation keys (or translated strings from t(...)), and
modify where moodDescription is defined to call the same translation function
with an appropriate key; keep the emoji characters as-is and only localize
human-readable text/ARIA labels.

Comment on lines +26 to +40
const moodDescription = `Mood: ${mood.label} (${moodScore})`;

return (
<Box {...styles.container}>
<Box
{...styles.container}
px="3"
gap="2"
title={moodDescription}
>
<Text {...styles.text}>{t(`aiState.${aiState}`)}</Text>
<Text
fontSize="14px"
lineHeight="1"
aria-label={moodDescription}
>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the new mood accessibility text.

moodDescription is hardcoded English, so tooltip/ARIA text won’t match the active locale.

💡 Suggested fix
-  const moodDescription = `Mood: ${mood.label} (${moodScore})`;
+  const moodLabel = t(`mood.${mood.label}`, { defaultValue: mood.label });
+  const moodDescription = t('mood.description', {
+    defaultValue: 'Mood: {{label}} ({{score}})',
+    label: moodLabel,
+    score: moodScore,
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const moodDescription = `Mood: ${mood.label} (${moodScore})`;
return (
<Box {...styles.container}>
<Box
{...styles.container}
px="3"
gap="2"
title={moodDescription}
>
<Text {...styles.text}>{t(`aiState.${aiState}`)}</Text>
<Text
fontSize="14px"
lineHeight="1"
aria-label={moodDescription}
>
const moodLabel = t(`mood.${mood.label}`, { defaultValue: mood.label });
const moodDescription = t('mood.description', {
defaultValue: 'Mood: {{label}} ({{score}})',
label: moodLabel,
score: moodScore,
});
return (
<Box
{...styles.container}
px="3"
gap="2"
title={moodDescription}
>
<Text {...styles.text}>{t(`aiState.${aiState}`)}</Text>
<Text
fontSize="14px"
lineHeight="1"
aria-label={moodDescription}
>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/footer/ai-state-indicator.tsx` around lines 26 -
40, The tooltip/ARIA text is built from the hardcoded string moodDescription, so
localize it using the existing translation function (t) instead of embedding
English; create or use a translation key (e.g. aiState.moodDescription) and pass
mood.label and moodScore as interpolation params, then replace moodDescription
with the localized string for the Box title and the Text aria-label (references:
moodDescription variable, aiState usage, and the title/aria-label props in this
component).

<InputGroup>
<Box position="relative" width="100%">
<IconButton
aria-label="Attach file"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Localize the attach button label.

Line 111 hardcodes "Attach file" while adjacent UI uses translation keys; this leaves screen-reader text untranslated.

Suggested fix
-          <IconButton
-            aria-label="Attach file"
+          <IconButton
+            aria-label={t('footer.attachFile')}
             variant="ghost"
             {...footerStyles.footer.attachButton}
             onClick={() => fileInputRef.current?.click()}
           >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/footer/footer.tsx` at line 111, The aria-label on
the attach button is hardcoded as "Attach file"; replace it with the localized
string by using the project's i18n function (e.g., the useTranslation hook and
its t(...) call or i18n.t(...)) inside the Footer component/Attach button JSX so
the screen-reader label uses a translation key (e.g., "attach_file" or the
existing keyspace) instead of a literal string; update the JSX attribute
aria-label={t('your_translation_key_here')} and ensure useTranslation is
imported and initialized in footer.tsx.

<HStack gap={`${THUMBNAIL_GAP}px`} flexWrap="nowrap" align="center">
{attachedImages.map((image, index) => (
<Box
key={`${image.data}-${index}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid using base64 payload as a React key.

Line 214 uses the full image data URL as part of the key. That can be very large and increases render/memory overhead for attachment-heavy chats.

Suggested fix
-                  key={`${image.data}-${index}`}
+                  key={`${index}-${image.mime_type}`}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
key={`${image.data}-${index}`}
key={`${index}-${image.mime_type}`}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/footer/footer.tsx` at line 214, The React list
key currently uses the entire base64 data URL (key={`${image.data}-${index}`})
which is large; change the key in the footer image list rendering to use a
stable, small identifier instead — e.g., use a provided unique id on the image
object (image.id) or fallback to the index (index) or a short deterministic
hash/slice of image.data (first N chars) so the key is compact and stable;
update the map/render code in the footer component where image.data and index
are referenced (the expression producing the key) to use one of these
alternatives.

Comment on lines +917 to +940
private async loadClip(resolvedUrl: string): Promise<ParsedIdleClip | null> {
const cached = this.clipCache.get(resolvedUrl);
if (cached) {
return cached;
}

const loader = (async () => {
try {
const response = await fetch(resolvedUrl);
if (!response.ok) {
throw new Error(`Failed to fetch idle clip: ${response.status}`);
}

const motion3 = await response.json() as Motion3File;
const parsed = parseMotion3Clip(resolvedUrl, motion3);
return parsed;
} catch (error) {
console.warn('[RecordedIdleDriver] failed to load idle clip:', resolvedUrl, error);
return null;
}
})();

this.clipCache.set(resolvedUrl, loader);
return loader;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t cache failed idle-clip loads permanently.

loadClip() stores the promise in clipCache before the request finishes, but failures return null without evicting that entry. One transient fetch/parse error then poisons the URL for the rest of the session, because every later selection reuses the cached null instead of retrying.

Suggested fix
   private async loadClip(resolvedUrl: string): Promise<ParsedIdleClip | null> {
     const cached = this.clipCache.get(resolvedUrl);
     if (cached) {
       return cached;
     }

     const loader = (async () => {
       try {
         const response = await fetch(resolvedUrl);
         if (!response.ok) {
           throw new Error(`Failed to fetch idle clip: ${response.status}`);
         }

         const motion3 = await response.json() as Motion3File;
         const parsed = parseMotion3Clip(resolvedUrl, motion3);
+        if (!parsed) {
+          this.clipCache.delete(resolvedUrl);
+        }
         return parsed;
       } catch (error) {
+        this.clipCache.delete(resolvedUrl);
         console.warn('[RecordedIdleDriver] failed to load idle clip:', resolvedUrl, error);
         return null;
       }
     })();

     this.clipCache.set(resolvedUrl, loader);
     return loader;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private async loadClip(resolvedUrl: string): Promise<ParsedIdleClip | null> {
const cached = this.clipCache.get(resolvedUrl);
if (cached) {
return cached;
}
const loader = (async () => {
try {
const response = await fetch(resolvedUrl);
if (!response.ok) {
throw new Error(`Failed to fetch idle clip: ${response.status}`);
}
const motion3 = await response.json() as Motion3File;
const parsed = parseMotion3Clip(resolvedUrl, motion3);
return parsed;
} catch (error) {
console.warn('[RecordedIdleDriver] failed to load idle clip:', resolvedUrl, error);
return null;
}
})();
this.clipCache.set(resolvedUrl, loader);
return loader;
private async loadClip(resolvedUrl: string): Promise<ParsedIdleClip | null> {
const cached = this.clipCache.get(resolvedUrl);
if (cached) {
return cached;
}
const loader = (async () => {
try {
const response = await fetch(resolvedUrl);
if (!response.ok) {
throw new Error(`Failed to fetch idle clip: ${response.status}`);
}
const motion3 = await response.json() as Motion3File;
const parsed = parseMotion3Clip(resolvedUrl, motion3);
if (!parsed) {
this.clipCache.delete(resolvedUrl);
}
return parsed;
} catch (error) {
this.clipCache.delete(resolvedUrl);
console.warn('[RecordedIdleDriver] failed to load idle clip:', resolvedUrl, error);
return null;
}
})();
this.clipCache.set(resolvedUrl, loader);
return loader;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/live2d/mixer/recorded-idle-driver.ts` around lines 917 -
940, loadClip currently stores the in-flight promise into clipCache immediately,
but if the fetch/parse fails the stored promise resolves to null and poisons the
cache; change loadClip (and the local loader promise handling) so that you
either (a) remove/evict the cache entry when the loader resolves to null or
throws, or (b) only set clipCache.set(resolvedUrl, parsed) after a successful
parse (and don't cache null). Specifically update loadClip/loader to catch
failures and call this.clipCache.delete(resolvedUrl) (or avoid setting the cache
until parsed is available) so subsequent calls will retry instead of returning a
cached null.

Comment on lines +125 to 131
"failedReadFile": "读取附件{{filename}}失败",
"failedParseWebSocket": "解析WebSocket消息失败",
"unsupportedFileType": "仅支持上传图片文件",
"websocketNotOpen": "WebSocket未连接",
"vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)"
"vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)",
"maxAttachmentsExceeded": "最多只能上传{{max}}个附件"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add missing error.ttsGenerationFailed key in zh locale.

src/renderer/src/locales/en/translation.json includes error.ttsGenerationFailed, but this locale file does not, causing inconsistent localized error rendering.

Proposed fix
   "error": {
     "cameraApiNotSupported": "此设备不支持摄像头API",
@@
     "failedReadFile": "读取附件{{filename}}失败",
     "failedParseWebSocket": "解析WebSocket消息失败",
+    "ttsGenerationFailed": "该句语音生成失败,已跳过。",
     "unsupportedFileType": "仅支持上传图片文件",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"failedReadFile": "读取附件{{filename}}失败",
"failedParseWebSocket": "解析WebSocket消息失败",
"unsupportedFileType": "仅支持上传图片文件",
"websocketNotOpen": "WebSocket未连接",
"vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)"
"vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)",
"maxAttachmentsExceeded": "最多只能上传{{max}}个附件"
},
"failedReadFile": "读取附件{{filename}}失败",
"failedParseWebSocket": "解析WebSocket消息失败",
"ttsGenerationFailed": "该句语音生成失败,已跳过。",
"unsupportedFileType": "仅支持上传图片文件",
"websocketNotOpen": "WebSocket未连接",
"vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)",
"maxAttachmentsExceeded": "最多只能上传{{max}}个附件"
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/locales/zh/translation.json` around lines 125 - 131, The zh
locale is missing the error key error.ttsGenerationFailed that exists in en;
open src/renderer/src/locales/zh/translation.json and add an entry for
"error.ttsGenerationFailed" with an appropriate Chinese translation (e.g. a
short message indicating TTS generation failed) alongside the other error keys
(near failedReadFile, failedParseWebSocket, etc.) so the key names match exactly
with the en locale.

Comment on lines +529 to +531
if (!message.turn_id || !currentTurnIdRef.current || message.turn_id === currentTurnIdRef.current) {
setBackendSynthComplete(true);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten backend-synth-complete turn validation.

Current logic accepts completion when there is no active local turn ID, which can acknowledge stale turn completions after a turn reset.

💡 Suggested fix
-        if (!message.turn_id || !currentTurnIdRef.current || message.turn_id === currentTurnIdRef.current) {
+        if (!message.turn_id || message.turn_id === currentTurnIdRef.current) {
           setBackendSynthComplete(true);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!message.turn_id || !currentTurnIdRef.current || message.turn_id === currentTurnIdRef.current) {
setBackendSynthComplete(true);
}
if (!message.turn_id || message.turn_id === currentTurnIdRef.current) {
setBackendSynthComplete(true);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/services/websocket-handler.tsx` around lines 529 - 531, The
current check lets a backend synth-complete fire when there's no active local
turn; tighten it by requiring a matching turn_id: in the websocket handler
change the condition so setBackendSynthComplete(true) is only called when
message.turn_id is truthy AND currentTurnIdRef.current is truthy AND
message.turn_id === currentTurnIdRef.current; use the existing identifiers
(message.turn_id, currentTurnIdRef.current, setBackendSynthComplete) and
ignore/skip completions when the local turn ID is absent or mismatched.

Comment on lines +104 to 121
score?: number;
members?: string[];
is_owner?: boolean;
client_uid?: string;
forwarded?: boolean;
display_text?: DisplayText;
turn_id?: string;
tts_error?: boolean;
live2d_model?: string;
expression?: string | number;
idle_state?: string;
idle_list?: string[] | null;
idle_mode?: IdlePlaybackMode;
idle_bank?: IdleBankConfig | null;
mixer_weights?: Partial<Record<PoseLayerId, number>>;
mixer_weights_mode?: 'patch' | 'reset';
idle_play?: IdlePlayCommand | string | null;
browser_view?: {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add missing asr_enabled to MessageEvent contract.

MessageEvent is now consumed with message.asr_enabled (Line 367 in src/renderer/src/services/websocket-handler.tsx), but this field is missing from the interface here.

💡 Suggested fix
 export interface MessageEvent {
@@
   client_uid?: string;
+  asr_enabled?: boolean;
   forwarded?: boolean;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
score?: number;
members?: string[];
is_owner?: boolean;
client_uid?: string;
forwarded?: boolean;
display_text?: DisplayText;
turn_id?: string;
tts_error?: boolean;
live2d_model?: string;
expression?: string | number;
idle_state?: string;
idle_list?: string[] | null;
idle_mode?: IdlePlaybackMode;
idle_bank?: IdleBankConfig | null;
mixer_weights?: Partial<Record<PoseLayerId, number>>;
mixer_weights_mode?: 'patch' | 'reset';
idle_play?: IdlePlayCommand | string | null;
browser_view?: {
score?: number;
members?: string[];
is_owner?: boolean;
client_uid?: string;
asr_enabled?: boolean;
forwarded?: boolean;
display_text?: DisplayText;
turn_id?: string;
tts_error?: boolean;
live2d_model?: string;
expression?: string | number;
idle_state?: string;
idle_list?: string[] | null;
idle_mode?: IdlePlaybackMode;
idle_bank?: IdleBankConfig | null;
mixer_weights?: Partial<Record<PoseLayerId, number>>;
mixer_weights_mode?: 'patch' | 'reset';
idle_play?: IdlePlayCommand | string | null;
browser_view?: {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/services/websocket-service.tsx` around lines 104 - 121, The
MessageEvent interface in websocket-service.tsx is missing the asr_enabled
property which is later accessed as message.asr_enabled in
websocket-handler.tsx; update the MessageEvent type (the interface that contains
score/members/is_owner/.../browser_view) to include asr_enabled?: boolean (or
appropriate type) so consumers like websocket-handler.tsx can safely read
message.asr_enabled without TypeScript errors.

// Model directory and filename storage
export let ModelDir: string[] = [];
export let ModelFileNames: string[] = []; // New array to store model file names
export let CurrentIdleMotionGroupName: string | null = 'Idle';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the default idle group when no override is provided.

updateModelConfig() now turns an omitted idleMotionGroupName into null, so any model that relied on the existing 'Idle' default stops entering idle altogether. src/renderer/WebSDK/src/lappmodel.ts:553-565 only starts idle motion when CurrentIdleMotionGroupName is truthy, so this regresses the default path.

Suggested fix
-  CurrentIdleMotionGroupName = idleMotionGroupName ?? null;
+  CurrentIdleMotionGroupName = idleMotionGroupName?.trim() || MotionGroupIdle;

Also applies to: 42-62

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/WebSDK/src/lappdefine.ts` at line 39, The updateModelConfig flow
is turning an omitted idleMotionGroupName into null which breaks the default
Idle behavior because lappmodel.ts only starts idle when
CurrentIdleMotionGroupName is truthy; update the logic so
CurrentIdleMotionGroupName retains the 'Idle' default when no override is
provided: either (a) initialize/export CurrentIdleMotionGroupName as 'Idle' and
ensure updateModelConfig does not overwrite it with null when
idleMotionGroupName is undefined, or (b) if updateModelConfig must assign, treat
undefined as 'Idle' instead of null; refer to CurrentIdleMotionGroupName and
updateModelConfig (and the checks in lappmodel.ts) when making the change.

@MrXnneHang MrXnneHang closed this Jun 7, 2026
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.

2 participants