-
Notifications
You must be signed in to change notification settings - Fork 449
refactor: enhance AI block UI/UX #2103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Overhaul chat interface with improved message containers and styling - Enhance responsive layout for better space utilization - Redesign typing indicator with new animation and visual appearance - Add message actions for copy, edit, and repeat functionality
- Implement sending code to terminal
WalkthroughThe PR updates multiple frontend components: markdown styles for codeblock actions now use opacity transitions, repositioning, and new icon button styles; the CodeBlock component adds methods and UI to send code to terminal blocks via a context menu and RPC calls; typing indicator SCSS/TSX were reworked into a typed, exportable React component with a bubble/dot animation and style prop; WaveAI chat styles and TSX were reorganized—message layout, model/preset selector, per-message editing/copy actions, and WaveAiModel state were modified (including removal of viewText and addition of noPadding). Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (3)
frontend/app/element/markdown.tsx (1)
97-144
: Implemented terminal text sending with context menuThe handleSendToTerminal function effectively:
- Collects text from code blocks
- Finds existing terminal blocks
- Creates a well-structured context menu
- Includes option to create a new terminal
The implementation follows good patterns for context menu creation, but the function is quite lengthy and could be refactored into smaller, more focused functions.
Consider breaking this large function into smaller helper functions like
getAvailableTerminals()
andbuildTerminalMenu()
for better maintainability.frontend/app/view/waveai/waveai.tsx (1)
358-584
: Smooth and well-structured editing logic.
Your new local states (editing
,editText
,copied
) and respective handlers (focus, copy to clipboard, and text area resizing) are implemented cleanly. The approach of re-prompting after editing (lines 423-440) looks intentional. However, note that each re-submit can lead to a new backend call, potentially consuming additional tokens or resources.If the cost or resource usage becomes an issue, consider an alternative strategy, such as partial in-memory edits without triggering a re-prompt until the user explicitly requests it.
frontend/app/view/waveai/waveai.scss (1)
47-60
: Refined chat message padding and spacing.
The added padding around messages (e.g.,padding: 14px 16px 8px
) should enhance readability. Be mindful of potential overlap if new action buttons or other elements are added in the future.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
frontend/app/element/markdown.scss
(1 hunks)frontend/app/element/markdown.tsx
(4 hunks)frontend/app/element/typingindicator.scss
(1 hunks)frontend/app/element/typingindicator.tsx
(1 hunks)frontend/app/view/waveai/waveai.scss
(3 hunks)frontend/app/view/waveai/waveai.tsx
(9 hunks)
🧰 Additional context used
🧬 Code Definitions (2)
frontend/app/element/markdown.tsx (4)
frontend/app/store/global.ts (3)
getAllBlockComponentModels
(771-771)globalStore
(783-783)atoms
(762-762)frontend/app/store/wshclientapi.ts (1)
RpcApi
(492-492)frontend/util/util.ts (1)
stringToBase64
(401-401)frontend/app/element/iconbutton.tsx (1)
IconButton
(12-34)
frontend/app/view/waveai/waveai.tsx (6)
frontend/app/view/webview/webview.tsx (1)
handleKeyDown
(310-321)frontend/util/util.ts (1)
fireAndForget
(388-388)frontend/app/store/services.ts (2)
BlockService
(23-23)ObjectService
(88-88)frontend/app/element/typingindicator.tsx (1)
TypingIndicator
(12-22)frontend/app/store/global.ts (1)
getApi
(772-772)pkg/waveobj/wtype.go (1)
BlockDef
(241-244)
🔇 Additional comments (23)
frontend/app/element/typingindicator.tsx (1)
4-4
: Component refactored with improved TypeScript typingThe TypingIndicator component has been properly refactored to use modern React patterns and TypeScript features:
- Using a proper exported interface instead of a type
- Adding style prop support for inline styling customization
- Using React.FC typing for better type checking
- Improved HTML structure with semantic class names that match the CSS
These changes enhance component reusability and maintainability.
Also applies to: 7-10, 12-19
frontend/app/element/markdown.scss (3)
129-139
: Improved code block actions with modern UI effectsThe codeblock-actions styling has been enhanced with:
- Better positioning with 5px offsets from edges
- Smooth opacity transitions instead of abrupt visibility changes
- Semi-transparent background with blur effect
- Consistent spacing with gap property
These changes create a more polished and modern UI component.
140-148
: Added hover effects for better interaction feedbackWell-implemented styling for icon buttons with appropriate sizing and hover state feedback. The 14px font size and 3px padding create a compact but usable button, while the hover effect provides clear user feedback.
152-153
: Proper transition handling on hoverThe hover state change from opacity:0 to opacity:1 pairs well with the transition definition, creating a smooth fade-in effect.
frontend/app/element/markdown.tsx (3)
12-15
: Added necessary imports for new terminal functionalityThe new imports support the added "Send to Terminal" feature, correctly importing the required dependencies for context menus, RPC API communication, and terminal block management.
Also applies to: 27-27
146-153
: Well-implemented terminal text sending functionThe sendTextToTerminal function correctly:
- Appends a newline to simulate Enter key press
- Base64 encodes the text for proper transmission
- Uses the appropriate RPC API call to send the input
This implementation ensures reliable text transfer to terminal blocks.
169-176
: Added user-friendly terminal button to code blocksGreat addition of a terminal icon button that allows users to send code block text to terminal instances. The button uses consistent styling with other action buttons and provides a helpful tooltip.
frontend/app/element/typingindicator.scss (4)
4-9
: Redesigned typing indicator with flexible layoutThe new typing indicator uses an inline-flex layout with appropriate spacing, making it more flexible for different UI contexts while maintaining consistent alignment.
11-19
: Added stylish bubble container for typing indicatorThe bubble design with semi-transparent background derived from the accent color creates a more polished and visually appealing container for the typing dots. The rounded corners and padding provide good visual separation.
21-42
: Improved animation timing for typing dotsThe dots now have consistent size and styling with a staggered animation effect that creates a natural typing rhythm. Using the accent color with opacity ensures the indicator matches the overall UI theme.
45-54
: Enhanced animation with vertical movementThe typing-animation keyframes create a smooth up-and-down bouncing effect that's more visually interesting than the previous animation. The combination of position change and opacity adjustment creates a natural-looking typing indicator.
frontend/app/view/waveai/waveai.tsx (4)
85-99
: Consider confirming usage or removingnoPadding
.
You introduced thenoPadding
atom at line 85 and set it totrue
at line 99, but there doesn't appear to be a direct usage within the file. It may be a placeholder for future styling. If it's unused, consider removing it or add references to ensure it's functional.Could you verify whether other files in the project reference
noPadding
? If not, it might be safe to remove this property.
700-730
: NewonButtonPress
andlocked
props.
The extension ofChatInputProps
and integration of these props offers clearer separation of concerns for the chat button state. This approach is straightforward and makes the button logic more transparent.
760-794
: Model menu handling and addition logic.
Your code for toggling the preset menu, selecting a model, and adding a new preset is cohesive. The external click detection in lines 719-730 is a common pattern and appears correct.
811-859
: Responsive input container and usage oflocked
.
The updated markup in theChatInput
component and how it's placed in the finalWaveAi
layout look solid. The submit button changing icons based onlocked
neatly communicates the ongoing or stoppable operation.Also applies to: 1030-1040
frontend/app/view/waveai/waveai.scss (8)
29-46
: Improved container layout and visual separators.
You switched to a column layout (flex-direction: column
) and added a bottom border to separate messages. This provides a clearer distinction between messages. Good move.
75-79
: Assistant message code blocks styling.
The background color and border radius around<pre>
improves the distinction for code snippets. The alpha-based color usage is consistent with the rest of your theme.Also applies to: 82-105
108-109
: User and edit message classes.
The.chat-msg-user
and.chat-msg-edit
classes correctly apply different styling needs. Theedit-input
focusing on transparent background with no border is a neat approach that feels minimalistic.Also applies to: 111-126
145-147
: Enhanced typing indicator spacing.
Increased padding around the typing indicator (lines 145-147) improves visual alignment. Good tiny UX detail.
149-191
: Action buttons layout with.msg-actions
.
The flex-based approach for right-alignment of the action buttons is tidy, and the hover effect that changes tovar(--accent-color)
is a thoughtful UX touch.
197-206
:.waveai-input-container
refactor.
Renaming from.waveai-controls
and reorganizing into a column layout clarifies the architecture. The slightly translucent background and border signals a distinct interactive area, aligning well with the chat theme.Also applies to: 209-227
229-300
: Preset selector UI enhancements.
The new.preset-selector
block with a dropdown (.model-menu
) is concise, ensuring intuitive user discovery of model options. The alpha-based hover effect on.model-menu-item
maintains stylistic consistency.
302-338
: Streamlined submit button styling.
Switching to a circular design with color-coded feedback for normal vs. stop states is modern and visually consistent. The new transitions in lines 313 and 322 ensure a smooth hover experience.
const tabId = globalStore.get(atoms.staticTabId); | ||
const oref = await RpcApi.CreateBlockCommand(TabRpcClient, { | ||
tabid: tabId, | ||
blockdef: termBlockDef, | ||
}); | ||
|
||
const blockId = oref.split(":")[1]; | ||
setTimeout(() => sendTextToTerminal(blockId, textToSend), 500); | ||
} catch (error) { | ||
console.error("Failed to create new terminal block:", error); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fixed potential issue with hardcoded timeout
The 500ms hardcoded timeout when creating a new terminal may not always be sufficient for terminal initialization, which could lead to missed text input.
Consider implementing a callback or event-based approach instead of a timeout to ensure the terminal is ready before sending text:
- setTimeout(() => sendTextToTerminal(blockId, textToSend), 500);
+ // Use an event listener or callback to detect when terminal is ready
+ const checkTerminalReady = () => {
+ const terminal = getAllBlockComponentModels().find(
+ bcm => bcm?.viewModel?.blockId === blockId && bcm?.viewModel?.viewType === "term"
+ );
+ if (terminal && terminal.isReady) {
+ sendTextToTerminal(blockId, textToSend);
+ } else {
+ setTimeout(checkTerminalReady, 100);
+ }
+ };
+ checkTerminalReady();
📝 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 tabId = globalStore.get(atoms.staticTabId); | |
const oref = await RpcApi.CreateBlockCommand(TabRpcClient, { | |
tabid: tabId, | |
blockdef: termBlockDef, | |
}); | |
const blockId = oref.split(":")[1]; | |
setTimeout(() => sendTextToTerminal(blockId, textToSend), 500); | |
} catch (error) { | |
console.error("Failed to create new terminal block:", error); | |
} | |
const tabId = globalStore.get(atoms.staticTabId); | |
const oref = await RpcApi.CreateBlockCommand(TabRpcClient, { | |
tabid: tabId, | |
blockdef: termBlockDef, | |
}); | |
const blockId = oref.split(":")[1]; | |
// Use an event listener or callback to detect when terminal is ready | |
const checkTerminalReady = () => { | |
const terminal = getAllBlockComponentModels().find( | |
bcm => bcm?.viewModel?.blockId === blockId && bcm?.viewModel?.viewType === "term" | |
); | |
if (terminal && terminal.isReady) { | |
sendTextToTerminal(blockId, textToSend); | |
} else { | |
setTimeout(checkTerminalReady, 100); | |
} | |
}; | |
checkTerminalReady(); | |
} catch (error) { | |
console.error("Failed to create new terminal block:", error); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (6)
frontend/app/element/typingindicator.tsx (2)
9-12
: Broaden props to standard div attributes for extensibility/a11y.Let consumers pass aria-, data-, id, onClick, etc., without changing this component again.
Apply:
-export interface TypingIndicatorProps { - style?: React.CSSProperties; - className?: string; -} +export interface TypingIndicatorProps extends React.HTMLAttributes<HTMLDivElement> {}
14-21
: Use clsx and wire through rest props; add basic a11y.
- Currently
clsx
is imported but unused; use it to compose classes safely.- Add
role="status"
andaria-live="polite"
for screen reader announcements.- Hide decorative dots from AT.
Apply:
-export const TypingIndicator: React.FC<TypingIndicatorProps> = ({ style, className }) => { +export const TypingIndicator: React.FC<TypingIndicatorProps> = ({ style, className, ...rest }) => { return ( - <div className={`typing-indicator ${className || ""}`} style={style}> - <div className="typing-indicator-bubble"> - <div className="typing-indicator-dot"></div> - <div className="typing-indicator-dot"></div> - <div className="typing-indicator-dot"></div> + <div + className={clsx("typing-indicator", className)} + style={style} + role="status" + aria-live="polite" + aria-label="Assistant is typing" + {...rest} + > + <div className="typing-indicator-bubble" aria-hidden="true"> + <div className="typing-indicator-dot" aria-hidden="true"></div> + <div className="typing-indicator-dot" aria-hidden="true"></div> + <div className="typing-indicator-dot" aria-hidden="true"></div> </div> </div> ); };frontend/app/view/waveai/waveai.tsx (4)
399-411
: Clipboard UX: clear timeout on unmount and guard API availability.Avoid state updates after unmount; add a small cleanup and feature detection.
Apply this diff:
@@ - const [copied, setCopied] = useState(false); + const [copied, setCopied] = useState(false); + const copyTimerRef = useRef<number | null>(null); @@ - useEffect(() => { + useEffect(() => { setEditText(text); }, [text]); + useEffect(() => { + return () => { + if (copyTimerRef.current != null) { + clearTimeout(copyTimerRef.current); + } + }; + }, []); @@ - const copyToClipboard = () => { - if (text) { - navigator.clipboard - .writeText(text) + const copyToClipboard = () => { + if (text && navigator.clipboard?.writeText) { + navigator.clipboard + .writeText(text) .then(() => { setCopied(true); - setTimeout(() => setCopied(false), 2000); + copyTimerRef.current = window.setTimeout(() => setCopied(false), 2000); }) .catch((err) => { console.error("Failed to copy text: ", err); }); } };Also applies to: 362-366, 375-378
838-841
: Comparator should handle equality deterministically.Current comparator returns -1 for equals; use numeric diff and name tiebreaker.
Apply this diff:
- .sort((a, b) => - (a[1]["display:order"] ?? 0) > (b[1]["display:order"] ?? 0) ? 1 : -1 - ) + .sort((a, b) => { + const ao = a[1]["display:order"] ?? 0; + const bo = b[1]["display:order"] ?? 0; + return ao - bo || String(a[1]["display:name"] ?? "").localeCompare(String(b[1]["display:name"] ?? "")); + })
853-859
: Button semantics & a11y.Add type="button" to avoid implicit submit behavior and aria-label for screen readers.
Apply this diff:
- <button + <button className={`waveai-submit-button ${locked ? "stop" : ""}`} onClick={onButtonPress} disabled={!locked && value.trim() === ""} + type="button" + aria-label={buttonTitle} > <i className={buttonIcon} title={buttonTitle} /> </button>
281-289
: Streaming cancel should propagate to backend.Breaking the for-await loop may not abort the RPC stream server-side. Consider adding an AbortController (or RPC cancel) and wiring model.cancel to signal it so the backend stops work and billing.
I can sketch an AbortController-based variant if StreamWaveAiCommand supports a signal option.
Also applies to: 1018-1025
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
frontend/app/element/typingindicator.tsx
(1 hunks)frontend/app/view/waveai/waveai.tsx
(10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/app/view/waveai/waveai.tsx (4)
frontend/util/util.ts (2)
fireAndForget
(411-411)makeIconClass
(421-421)frontend/app/store/services.ts (2)
BlockService
(23-23)ObjectService
(88-88)frontend/app/element/typingindicator.tsx (1)
TypingIndicator
(14-24)frontend/app/store/global.ts (3)
WOS
(809-809)getApi
(772-772)createBlock
(766-766)
🔇 Additional comments (3)
frontend/app/element/typingindicator.tsx (2)
4-4
: Prefer a type-only React import
Avoid a runtime import if you’re only using React types (React.FC
,React.CSSProperties
). Ensure your roottsconfig.json
is configured for the new JSX transform ("jsx": "react-jsx"
or"react-jsxdev"
) before applying this change.-import React from "react"; +import type React from "react";
17-21
: SCSS class names align:.typing-indicator-bubble
and.typing-indicator-dot
are defined intypingindicator.scss
.frontend/app/view/waveai/waveai.tsx (1)
85-85
: Verify noPadding is actually respected by the view host.You added model.noPadding = true but I don’t see it being read/used by the container. Please confirm the ViewModel contract consumes this flag; otherwise it’s dead state.
Also applies to: 99-100
const fontSize = useAtomValue(model.mergedPresets)?.["ai:fontsize"]; | ||
const fixedFontSize = useAtomValue(model.mergedPresets)?.["ai:fixedfontsize"]; | ||
const [editing, setEditing] = useState(false); | ||
const [editText, setEditText] = useState(text); | ||
const [copied, setCopied] = useState(false); | ||
const textAreaRef = useRef<HTMLTextAreaElement>(null); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid index keys; use message id for stable reconciliation.
ChatItem holds local UI state (editing/copied). Using key={idx} risks state bleed/shuffling on insert/remove. Use the message id as the key.
Apply this diff:
- {splitMessages.map((chitem, idx) => (
- <ChatItem key={idx} chatItemAtom={chitem} model={model} />
- ))}
+ {splitMessages.map((chitem) => {
+ const { id } = globalStore.get(chitem) as ChatMessageType;
+ return <ChatItem key={id} chatItemAtom={chitem} model={model} />;
+ })}
Also applies to: 685-687
🤖 Prompt for AI Agents
In frontend/app/view/waveai/waveai.tsx around lines 360-366 (and likewise at
~685-687), ChatItem instances are being keyed by array index which causes local
UI state (editing/copied) to bleed when items are inserted/removed; replace
key={idx} with a stable identifier such as key={message.id} (or message?.id)
when rendering the list, ensure message.id is present and unique (or generate a
stable id if not), and update any typing or null checks accordingly so the
component uses a stable key for reconciliation.
const startEditing = () => { | ||
if (user === "user") { | ||
setEditing(true); | ||
setEditText(text); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Disable edit/repeat actions while a request is in-flight.
Prevents truncation races with the streaming loop and inconsistent history while locked.
Apply this diff:
@@
- const [editing, setEditing] = useState(false);
+ const [editing, setEditing] = useState(false);
const [editText, setEditText] = useState(text);
const [copied, setCopied] = useState(false);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
+ const locked = useAtomValue(model.locked);
@@
- const startEditing = () => {
- if (user === "user") {
+ const startEditing = () => {
+ if (user === "user" && !locked) {
setEditing(true);
setEditText(text);
}
};
@@
- <button
+ <button
className="msg-action-btn repeat-btn"
onClick={handleRepeat}
title="Repeat (deletes this and all following messages)"
+ disabled={locked}
>
@@
- <button className="msg-action-btn edit-btn" onClick={cancelEditing} title="Cancel">
+ <button className="msg-action-btn edit-btn" onClick={cancelEditing} title="Cancel">
@@
- <button className="msg-action-btn copy-btn" onClick={saveEdit} title="Save">
+ <button className="msg-action-btn copy-btn" onClick={saveEdit} title="Save" disabled={locked}>
@@
- <button
+ <button
className="msg-action-btn repeat-btn"
onClick={handleRepeat}
title="Repeat (deletes this and all following messages)"
+ disabled={locked}
>
@@
- <button className="msg-action-btn edit-btn" onClick={startEditing} title="Edit">
+ <button className="msg-action-btn edit-btn" onClick={startEditing} title="Edit" disabled={locked}>
Also applies to: 515-547, 562-580
🤖 Prompt for AI Agents
In frontend/app/view/waveai/waveai.tsx around lines 413-418 (and also apply the
same change to 515-547 and 562-580), the edit/repeat actions are allowed even
while a request/stream is in-flight which causes truncation races and
inconsistent history; add a guard using the existing in-flight/streaming state
(e.g. isRequestInFlight, isStreaming, or locked flag used elsewhere) so these
handlers return early when a request is active, and ensure UI controls are also
disabled while in-flight; update startEditing (and the analogous repeat/edit
handlers in the other ranges) to check the flag before calling
setEditing/setEditText (or performing repeat) so edits/repeats are only possible
when no request is outstanding.
const saveEdit = () => { | ||
if (editText.trim() === "") { | ||
return; | ||
} | ||
|
||
setEditing(false); | ||
fireAndForget(async () => { | ||
const history = await model.fetchAiData(); | ||
const msgIndex = history.findIndex((msg) => msg.role === user && msg.content === text); | ||
|
||
if (msgIndex !== -1) { | ||
const updatedHistory = history.slice(0, msgIndex); | ||
await BlockService.SaveWaveAiData(model.blockId, updatedHistory); | ||
await model.populateMessages(); | ||
model.sendMessage(editText, user); | ||
} | ||
}); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Editing/Repeat may target the wrong message when duplicates exist.
Both flows locate the message by role+content using findIndex, which can hit an earlier duplicate. Prefer the most recent match.
Apply this diff to search from the end in both places:
- const msgIndex = history.findIndex((msg) => msg.role === user && msg.content === text);
+ const msgIndex = (() => {
+ for (let i = history.length - 1; i >= 0; i--) {
+ if (history[i].role === user && history[i].content === text) return i;
+ }
+ return -1;
+ })();
Also applies to: 443-457
🤖 Prompt for AI Agents
In frontend/app/view/waveai/waveai.tsx around lines 424-441 (and similarly for
443-457), the code finds the target message using history.findIndex(msg =>
msg.role === user && msg.content === text) which picks the first match and can
target an earlier duplicate; change the lookup to find the most recent match by
using a reverse search (e.g., history.findLastIndex with the same predicate or
iterate from the end) so you get the last occurrence, then slice/update based on
that index in both edit and repeat flows.
#2104