Add reusable OzwellChat component - #368
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new reusable, UI-only OzwellChat widget shell to @mieweb/ui, plus Storybook demos, and adjusts AIChat auto-scrolling to scroll only within its own message container (avoiding Storybook/Docs page scrolling).
Changes:
- Introduces
OzwellChat(thinking controls, message navigation, warning toast, model selector integration) built on top ofAIChat. - Adds comprehensive Storybook stories including an interactive local mock conversation and state explorer.
- Updates
AIChatauto-scroll implementation to use the internal messages container ref.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/components/AI/OzwellChatView.tsx | New OzwellChat UI shell component; thinking/menu/model selector/warning/footer composition. |
| src/components/AI/OzwellChat.stories.tsx | Storybook documentation, state explorer, and interactive playground for OzwellChat. |
| src/components/AI/index.ts | Public exports for OzwellChat and associated types. |
| src/components/AI/AIChat.tsx | Adjusts auto-scroll behavior to scroll only the messages container. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/components/AI/OzwellChatView.tsx:125
OzwellChatis exported from a file namedOzwellChatView.tsx. This*Viewsuffix isn’t used elsewhere insrc/components/AIand makes it harder to find the component by name (and to grep/auto-import reliably). Consider renaming the file toOzwellChat.tsxand updating the barrel/story imports accordingly.
export function OzwellChat({
messages,
isGenerating = false,
inputPlaceholder = 'Ask a question...',
onSendMessage,
src/components/AI/OzwellChatView.tsx:119
applyThinkingModecontains non-trivial transformation/filtering logic (e.g., hiding thinking blocks, auto-collapsing based on streaming status, and dropping empty messages). There aren’t any unit tests for this behavior yet, which makes regressions in chat rendering likely as the adapter evolves. Adding a focusedOzwellChatView.test.tsx(or extracting this function to a tested helper) would improve confidence.
function applyThinkingMode(
messages: AIMessage[],
mode: OzwellThinkingMode
): AIMessage[] {
return messages
.map((message) => {
const hasTextContent = message.content.some(
(block) => block.type === 'text' && Boolean(block.text)
);
const content = message.content
.filter((block) => mode !== 'never' || block.type !== 'thinking')
.map((block) => {
if (block.type !== 'thinking') return block;
return {
...block,
collapsed:
mode === 'collapsed' ||
(mode === 'auto' &&
(message.status !== 'streaming' || hasTextContent)),
};
});
return { ...message, content };
})
.filter(
(message) =>
message.status === 'streaming' ||
message.content.length > 0 ||
message.role === 'tool'
);
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/components/AI/OzwellChatView.tsx:119
applyThinkingModeintroduces non-trivial behavior (hiding/collapsing thinking blocks and filtering empty messages) but the PR adds no unit tests to lock down these rules. Since this is a UI-library component, a small RTL/Vitest test suite would help prevent regressions when message types/statuses evolve.
function applyThinkingMode(
messages: AIMessage[],
mode: OzwellThinkingMode
): AIMessage[] {
return messages
.map((message) => {
const hasTextContent = message.content.some(
(block) => block.type === 'text' && Boolean(block.text)
);
const content = message.content
.filter((block) => mode !== 'never' || block.type !== 'thinking')
.map((block) => {
if (block.type !== 'thinking') return block;
return {
...block,
collapsed:
mode === 'collapsed' ||
(mode === 'auto' &&
(message.status !== 'streaming' || hasTextContent)),
};
});
return { ...message, content };
})
.filter(
(message) =>
message.status === 'streaming' ||
message.content.length > 0 ||
message.role === 'tool'
);
}
src/components/AI/OzwellChatView.tsx:362
inputPlaceholderis documented as a host-controlled placeholder, but when the model selector is shown the component always forces'Ask a question...', ignoring the prop value. This makes it impossible for consumers to customize placeholder text whenevermodelsare enabled.
inputPlaceholder={
showModelSelector ? 'Ask a question...' : inputPlaceholder
}
577b6f0 to
ca00952
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/components/AI/AIChat.tsx:337
- Auto-scroll sets
scrollToptoscrollHeight, which relies on browser clamping (the maximum scrollTop isscrollHeight - clientHeight). Setting the computed max explicitly is more correct and avoids edge cases when scrollHeight is smaller than the viewport.
// Auto-scroll to bottom on new messages
React.useEffect(() => {
const container = messagesContainerRef.current;
if (container) container.scrollTop = container.scrollHeight;
}, [messages]);
|
Addressed in 8faf0a9:\n\n- Replaced fixed Ozwell colors with semantic theme tokens.\n- Moved the message-flare keyframes and animation to src/tailwind-preset.ts, using the primary token instead of an inline style.\n\nLocal Storybook build passes. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/components/AI/OzwellChatView.tsx:280
OzwellChatalways passes arenderTextContentfunction toAIChat(viarenderMessageTextContent). When the host does not supplyrenderTextContent, this wrapper returns a raw string, which causesAIChatto take the custom-renderer path and lose its defaultwhitespace-pre-wraphandling (newlines will collapse). Preserve the default behavior when no host renderer is provided.
const renderMessageTextContent: AIRenderTextContent = (text, context) => {
if (context.messageId === QUEUED_MESSAGE_ID && isEditingQueuedMessage) {
return (
<textarea
aria-label="Edit queued message"
ref={queuedMessageEditorRef}
className="border-border bg-card text-foreground focus-visible:ring-ring block min-h-20 w-full resize-y rounded border px-2 py-1.5 text-sm outline-none focus-visible:ring-2"
value={queuedMessageDraft}
onChange={(event) => setQueuedMessageDraft(event.target.value)}
onKeyDown={(event) => {
if (event.key === 'Escape') setIsEditingQueuedMessage(false);
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
saveQueuedMessage();
}
}}
/>
);
}
return renderTextContent?.(text, context) ?? text;
};
8faf0a9 to
1573a89
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/components/AI/OzwellChatView.tsx:492
OzwellChatPropsexposesinputPlaceholder, but when the model selector is shown the component ignores the prop and hardcodes'Ask a question...'. This makes the prop ineffective for the common “models enabled” state and is surprising for API consumers trying to customize placeholder copy.
inputPlaceholder={
showModelSelector ? 'Ask a question...' : inputPlaceholder
}
onSendMessage={onSendMessage}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/components/AI/OzwellChatView.tsx:66
QUEUED_MESSAGE_IDis a fixed string. If a host ever provides a real message with the sameid, React keys will collide and the queued-message special-casing (edit footer / textarea renderer) could apply to the wrong message. Consider using a more namespaced internal id to minimize collision risk.
const QUEUED_MESSAGE_ID = 'ozwell-queued-message';
src/components/AI/OzwellChatView.tsx:496
inputPlaceholderis ignored whenevershowModelSelectoris true (it always forces'Ask a question...'). This makes theinputPlaceholderprop ineffective for hosts that show the model selector.
inputPlaceholder={
showModelSelector ? 'Ask a question...' : inputPlaceholder
}


Closes #363
Summary
Adds the Ozwell widget directly to
@mieweb/ui, following the same UI-library approach as the Q chat component.OzwellChatcomponent and public exports.AIChatautomatic scrolling so it scrolls only its own message container instead of moving the surrounding Docs page.The Ozwell API adapter remains responsible for requests, streaming parsing, authentication, model discovery, tools, and iframe/window behavior.
Checks
pnpm typecheckpnpm lintgit diff --check origin/main...HEAD