Skip to content

refactor: decompose ChatPanel into focused modules#30

Closed
SweetSophia wants to merge 1 commit intoMiniMax-AI:mainfrom
SweetSophia:refactor/extract-chat-engine
Closed

refactor: decompose ChatPanel into focused modules#30
SweetSophia wants to merge 1 commit intoMiniMax-AI:mainfrom
SweetSophia:refactor/extract-chat-engine

Conversation

@SweetSophia
Copy link
Copy Markdown

Summary

The ChatPanel/index.tsx was a 1472-line god component handling LLM conversation loop, tool dispatch, character management, mod system, settings UI, chat history, image generation, memory, file tools, and session management.

This PR decomposes it into four focused modules without changing any behavior:

File Responsibility Lines
toolDefinitions.ts Tool defs, system prompt builder, config guard 133
ChatSubComponents.tsx CharacterAvatar, StageIndicator, ActionsTaken 178
SettingsModal.tsx LLM + image generation configuration UI 270
useConversationEngine.ts Conversation loop + tool dispatch hook 341

ChatPanel/index.tsx is now a thin shell (632 lines, −57%) that wires state to UI.

Benefits

  • Testability: Conversation engine is testable in isolation (pure function + callbacks)
  • Readability: Each file has a single clear responsibility
  • Maintainability: Future changes target specific modules without touching the whole
  • No breaking changes: Pure structural refactor, zero behavior changes

Verification

  • pnpm run lint — 0 errors
  • pnpm run build — succeeds
  • ✅ All existing tests pass

Notes

The useConversationEngine hook accepts refs and callbacks rather than depending on React state directly, keeping it decoupled and enabling unit testing without mounting components.

Decompose the 1472-line ChatPanel god component into four focused files:

- toolDefinitions.ts (133 lines): tool defs, system prompt builder, config guard
- ChatSubComponents.tsx (178 lines): CharacterAvatar, StageIndicator, ActionsTaken
- SettingsModal.tsx (270 lines): LLM + image generation configuration UI
- useConversationEngine.ts (341 lines): conversation loop + tool dispatch hook

ChatPanel/index.tsx reduced from 1472 → 632 lines (-57%), now a thin shell
that wires state to UI. No behavior changes — pure structural refactor.

Benefits:
- Conversation engine is testable in isolation
- Settings modal can be reasoned about independently
- Each file has a single clear responsibility
- Future PRs can target specific modules without touching the whole
Copilot AI review requested due to automatic review settings April 1, 2026 13:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the ChatPanel “god component” by extracting the conversation/tool loop, tool definitions/system prompt construction, settings UI, and several UI subcomponents into focused modules.

Changes:

  • Extracted the LLM conversation loop + tool dispatch into useConversationEngine.ts.
  • Moved tool definitions + system prompt building into toolDefinitions.ts.
  • Extracted UI pieces into ChatSubComponents.tsx and SettingsModal.tsx, leaving ChatPanel/index.tsx as a wiring layer.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/webuiapps/src/components/ChatPanel/index.tsx Reduced responsibilities; wires state to extracted engine and UI modules.
apps/webuiapps/src/components/ChatPanel/useConversationEngine.ts Encapsulates tool-call loop and dispatch logic behind callbacks/refs.
apps/webuiapps/src/components/ChatPanel/toolDefinitions.ts Centralizes tool defs, config guard, and system prompt builder.
apps/webuiapps/src/components/ChatPanel/SettingsModal.tsx Standalone LLM + image-gen configuration modal.
apps/webuiapps/src/components/ChatPanel/ChatSubComponents.tsx Extracted avatar/stage/actions UI and message rendering helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 472 to 476
className={styles.headerLeft}
onClick={() => setShowCharacterPanel(true)}
style={{ cursor: 'pointer' }}
>
<span className={styles.characterName}>{character.character_name}</span>
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The header click-target no longer shows the ChevronRight icon that existed previously, which is a visible UI/behavior change. If the PR intent is “no behavior changes”, consider re-adding the indicator (or update the PR description to acknowledge the UI change).

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +35
import { getMemoryToolDefinitions, isMemoryTool, executeMemoryTool } from '@/lib/memoryManager';
import { ModManager, saveModCollection } from '@/lib/modManager';
import {
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

There are two imports from '@/lib/modManager' (a type-only ModManager import earlier and a value ModManager import here). Consider consolidating into a single import (e.g., import { ModManager, saveModCollection, type ModCollection } ...) to avoid duplicate-import lint issues and keep imports clearer.

Copilot uses AI. Check for mistakes.
Comment on lines +237 to +244
ctx.callbacks.setModCollection((prev) => {
const updated = {
...prev,
items: { ...prev.items, [updatedEntry.config.id]: updatedEntry },
};
saveModCollection(updated);
return updated;
});
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

saveModCollection(updated) is called inside the setModCollection state-updater function. React expects state updater functions to be pure; in StrictMode/dev this updater can be invoked more than once, causing duplicate persistence/network calls. Move persistence out of the updater (e.g., compute the updated collection once, call setModCollection with it, then persist in a separate step/effect).

Suggested change
ctx.callbacks.setModCollection((prev) => {
const updated = {
...prev,
items: { ...prev.items, [updatedEntry.config.id]: updatedEntry },
};
saveModCollection(updated);
return updated;
});
let updatedCollection: any = null;
ctx.callbacks.setModCollection((prev) => {
const updated = {
...prev,
items: { ...prev.items, [updatedEntry.config.id]: updatedEntry },
};
updatedCollection = updated;
return updated;
});
if (updatedCollection) {
saveModCollection(updatedCollection);
}

Copilot uses AI. Check for mistakes.
Comment on lines +296 to +301
try {
const result = await executeMemoryTool(
ctx.sessionPathRef.current,
params as Record<string, string>,
);
loadMemories(ctx.sessionPathRef.current).then(ctx.callbacks.setMemories);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

loadMemories(ctx.sessionPathRef.current).then(setMemories) can race with a sessionPath change: if the session switches before the promise resolves, it may overwrite memories for the new session with stale data. Capture the path in a local variable and only apply the result if it still matches the latest sessionPathRef.current (or cancel/ignore out-of-date requests).

Suggested change
try {
const result = await executeMemoryTool(
ctx.sessionPathRef.current,
params as Record<string, string>,
);
loadMemories(ctx.sessionPathRef.current).then(ctx.callbacks.setMemories);
const sessionPath = ctx.sessionPathRef.current;
try {
const result = await executeMemoryTool(
sessionPath,
params as Record<string, string>,
);
loadMemories(sessionPath).then((memories) => {
if (ctx.sessionPathRef.current === sessionPath) {
ctx.callbacks.setMemories(memories);
}
});

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +131
setLayers((prev) => {
const staleUrls = prev.filter((l) => l.url !== readyUrl).map((l) => l.url);
setTimeout(() => {
setLayers((curr) => curr.filter((l) => !staleUrls.includes(l.url)));
}, 300);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

The setTimeout callback calls setLayers without any unmount cleanup. If the component unmounts within the 300ms delay, this can trigger a state update on an unmounted component. Track the timeout ID and clear it in an effect cleanup (or guard with an isMounted ref) to avoid leaks/warnings.

Copilot uses AI. Check for mistakes.
@SweetSophia
Copy link
Copy Markdown
Author

Closing to run internal review first. Will resubmit after validation.

@SweetSophia SweetSophia closed this Apr 1, 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