feat: add upload image for button, and can preview and remove image by one click.#19
feat: add upload image for button, and can preview and remove image by one click.#19MrXnneHang wants to merge 31 commits into
Conversation
…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
Summary of ChangesHello @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
🧠 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
Activity
Using Gemini Code AssistThe 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
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 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
|
| <HStack spacing="2" flexWrap="wrap"> | ||
| {attachedImages.map((image, index) => ( | ||
| <Box | ||
| key={`${image.data}-${index}`} |
There was a problem hiding this comment.
在 React 中,使用列表的索引 index 或者不稳定的数据(如这里的 base64 字符串 image.data)作为 key 是一种反模式,尤其是在列表项可以被增删的情况下。这可能会导致渲染问题和性能下降。
建议在上传图片时为每个图片生成一个唯一的客户端 ID,并用它作为 key。
你可以在 src/renderer/src/hooks/footer/use-text-input.tsx 中做如下修改:
-
更新
attachedImages的 state 类型,使其包含一个客户端 ID:const [attachedImages, setAttachedImages] = useState<(ImagePayload & { clientId: string })[]>([]);
-
在
handleAttachFiles函数中,当图片被读取时,为其生成一个唯一的 ID:// ... try { const dataUrl = await readFileAsDataUrl(file); newImages.push({ clientId: crypto.randomUUID(), // 生成唯一 ID source: 'upload', data: dataUrl, mime_type: file.type || 'image/*', }); } // ...
-
然后在这里,你就可以使用这个稳定的
clientId作为key:
| key={`${image.data}-${index}`} | |
| key={image.clientId} |
| <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); | ||
| }} | ||
| /> |
There was a problem hiding this comment.
这里的 IconButton 样式是内联定义的,并且包含中文注释。为了更好的代码组织和可维护性,建议将这些样式提取到 src/renderer/src/components/footer/footer-styles.tsx 文件中,并移除代码中的注释。
你可以在 footer-styles.tsx 的 FooterStyles 接口和 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" },
},
},
// ...
}然后在这里使用它,这样代码会更整洁。
| <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); | |
| }} | |
| /> |
| <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> |
There was a problem hiding this comment.
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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/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>
* 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>
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>
) 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>
📝 WalkthroughWalkthroughThis 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. ChangesLive2D Expression/Pose, Image Attachments, and Mood Features
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
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 winReset
persistentAppearanceon the clear-model path too.When
setModelInforeturns early for missingurl,modelInfois cleared butpersistentAppearanceis 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
idleMotionGroupNamechanges are ignored by the reload check.You added
modelInfo?.idleMotionGroupNameto the effect dependencies, butneedsUpdatestill only compares URL and scale. When just the idle group changes, this effect runs and then exits without callingupdateModelConfig(), 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 winReplace the clamp-based
bottomoffset with layout-driven spacing.The viewport-derived bottom shift is brittle and can misplace the footer across resolutions. Prefer keeping
bottom: 0and 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 winConditionally bind
onClickto matchisClickable.
onClickis 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
📒 Files selected for processing (48)
.github/PULL_REQUEST_TEMPLATE.md.gitignoresrc/main/index.tssrc/main/window-manager.tssrc/renderer/WebSDK/src/lappdefine.tssrc/renderer/WebSDK/src/lapplive2dmanager.tssrc/renderer/WebSDK/src/lappmodel.tssrc/renderer/src/App.tsxsrc/renderer/src/components/canvas/live2d.tsxsrc/renderer/src/components/canvas/ws-status.tsxsrc/renderer/src/components/electron/input-subtitle.tsxsrc/renderer/src/components/footer/ai-state-indicator.tsxsrc/renderer/src/components/footer/footer-styles.tsxsrc/renderer/src/components/footer/footer.tsxsrc/renderer/src/components/sidebar/chat-history-panel.tsxsrc/renderer/src/components/sidebar/sidebar-styles.tsxsrc/renderer/src/context/character-config-context.tsxsrc/renderer/src/context/chat-history-context.tsxsrc/renderer/src/context/live2d-config-context.tsxsrc/renderer/src/context/mood-context.tsxsrc/renderer/src/context/vad-context.tsxsrc/renderer/src/context/websocket-context.tsxsrc/renderer/src/hooks/canvas/live2d-parameter-layer-controller.tssrc/renderer/src/hooks/canvas/live2d-pose-mixer-controller.tssrc/renderer/src/hooks/canvas/use-live2d-appearance.tssrc/renderer/src/hooks/canvas/use-live2d-expression.tssrc/renderer/src/hooks/canvas/use-live2d-model.tssrc/renderer/src/hooks/canvas/use-live2d-pose-mixer.tssrc/renderer/src/hooks/canvas/use-live2d-resize.tssrc/renderer/src/hooks/canvas/use-ws-status.tssrc/renderer/src/hooks/footer/use-footer.tssrc/renderer/src/hooks/footer/use-text-input.tsxsrc/renderer/src/hooks/utils/use-audio-task.tssrc/renderer/src/hooks/utils/use-interrupt.tssrc/renderer/src/hooks/utils/use-media-capture.tsxsrc/renderer/src/hooks/utils/use-mic-toggle.tssrc/renderer/src/layout.tsxsrc/renderer/src/live2d/mixer/live2d-parameter-profile.tssrc/renderer/src/live2d/mixer/logical-channels.tssrc/renderer/src/live2d/mixer/pose-mixer.tssrc/renderer/src/live2d/mixer/recorded-idle-driver.tssrc/renderer/src/locales/en/translation.jsonsrc/renderer/src/locales/zh/translation.jsonsrc/renderer/src/services/websocket-handler.tsxsrc/renderer/src/services/websocket-service.tsxsrc/renderer/src/types/media.tssrc/renderer/src/utils/audio-manager.tssrc/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 让大家一起讨论 --> |
There was a problem hiding this comment.
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").
| 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' }; | ||
| } |
There was a problem hiding this comment.
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.
| 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} | ||
| > |
There was a problem hiding this comment.
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.
| 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" |
There was a problem hiding this comment.
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}`} |
There was a problem hiding this comment.
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.
| 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.
| 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; |
There was a problem hiding this comment.
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.
| 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.
| "failedReadFile": "读取附件{{filename}}失败", | ||
| "failedParseWebSocket": "解析WebSocket消息失败", | ||
| "unsupportedFileType": "仅支持上传图片文件", | ||
| "websocketNotOpen": "WebSocket未连接", | ||
| "vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)" | ||
| "vadMisfire": "检测到语音但过于简短,请尝试提高音量或说得更久一些,或调整识别设置(降低语音识别阈值、降低负面语音阈值、减少验证帧数)", | ||
| "maxAttachmentsExceeded": "最多只能上传{{max}}个附件" | ||
| }, |
There was a problem hiding this comment.
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.
| "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.
| if (!message.turn_id || !currentTurnIdRef.current || message.turn_id === currentTurnIdRef.current) { | ||
| setBackendSynthComplete(true); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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?: { |
There was a problem hiding this comment.
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.
| 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'; |
There was a problem hiding this comment.
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.
似乎原来的上传附件的 button click 事件没写。
这里添加了:
因为我后端对话历史是不存 image 的,所以:
clamp(1vh, calc(110px - 5vh), 1000vh)':这个只是做了近似,某些分辨率,在某些窗口大小下可能表现不佳。维护者似乎不在 =-=
但 PR 我先挂在这了。
Summary by CodeRabbit
New Features
Improvements