feat(vscode-ide-companion): refactor message components with modular architecture

Refactor UI message rendering by extracting message types into dedicated components. Add ChatHeader component for better session management interface.

- Extract message components: UserMessage, AssistantMessage, ThinkingMessage, StreamingMessage, WaitingMessage
- Add ChatHeader component with session selector and action buttons
- Delete MessageContent.css and consolidate styles into App.scss
- Update Tailwind config for component styling
- Improve message rendering with proper TypeScript typing
This commit is contained in:
yiliang114
2025-11-23 20:52:08 +08:00
parent c4bcd178a4
commit de8ea0678d
17 changed files with 901 additions and 535 deletions

View File

@@ -0,0 +1,42 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { MessageContent } from '../MessageContent.js';
interface AssistantMessageProps {
content: string;
timestamp: number;
onFileClick?: (path: string) => void;
}
export const AssistantMessage: React.FC<AssistantMessageProps> = ({
content,
timestamp: _timestamp,
onFileClick,
}) => (
<div className="flex gap-0 items-start text-left py-2 flex-col relative animate-[fadeIn_0.2s_ease-in]">
<div
className="inline-block my-1 relative whitespace-pre-wrap rounded-md max-w-full overflow-x-auto overflow-y-hidden select-text leading-[1.5]"
style={{
border: '1px solid var(--app-input-border)',
borderRadius: 'var(--corner-radius-medium)',
backgroundColor: 'var(--app-input-background)',
padding: '4px 6px',
color: 'var(--app-primary-foreground)',
}}
>
<MessageContent content={content} onFileClick={onFileClick} />
</div>
{/* Timestamp - temporarily hidden */}
{/* <div
className="text-xs opacity-60"
style={{ color: 'var(--app-secondary-foreground)' }}
>
{new Date(timestamp).toLocaleTimeString()}
</div> */}
</div>
);

View File

@@ -0,0 +1,39 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { MessageContent } from '../MessageContent.js';
interface StreamingMessageProps {
content: string;
onFileClick?: (path: string) => void;
}
export const StreamingMessage: React.FC<StreamingMessageProps> = ({
content,
onFileClick,
}) => (
<div className="flex gap-0 items-start text-left py-2 flex-col relative animate-[fadeIn_0.2s_ease-in]">
<div
className="inline-block my-1 relative whitespace-pre-wrap rounded-md max-w-full overflow-x-auto overflow-y-hidden select-text leading-[1.5]"
style={{
border: '1px solid var(--app-input-border)',
borderRadius: 'var(--corner-radius-medium)',
backgroundColor: 'var(--app-input-background)',
padding: '4px 6px',
color: 'var(--app-primary-foreground)',
}}
>
<MessageContent content={content} onFileClick={onFileClick} />
</div>
<div
className="absolute right-3 bottom-3 animate-[pulse_1.5s_ease-in-out_infinite]"
style={{ color: 'var(--app-primary-foreground)' }}
>
</div>
</div>
);

View File

@@ -0,0 +1,48 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { MessageContent } from '../MessageContent.js';
interface ThinkingMessageProps {
content: string;
timestamp: number;
onFileClick?: (path: string) => void;
}
export const ThinkingMessage: React.FC<ThinkingMessageProps> = ({
content,
timestamp: _timestamp,
onFileClick,
}) => (
<div className="flex gap-0 items-start text-left py-2 flex-col relative opacity-80 italic pl-6 animate-[fadeIn_0.2s_ease-in]">
<div
className="inline-block my-1 relative whitespace-pre-wrap rounded-md max-w-full overflow-x-auto overflow-y-hidden select-text leading-[1.5]"
style={{
backgroundColor:
'var(--app-list-hover-background, rgba(100, 100, 255, 0.1))',
border: '1px solid rgba(100, 100, 255, 0.3)',
borderRadius: 'var(--corner-radius-medium)',
padding: 'var(--app-spacing-medium)',
color: 'var(--app-primary-foreground)',
}}
>
<span className="inline-flex items-center gap-1 mr-2">
<span className="inline-block w-1.5 h-1.5 bg-[var(--app-secondary-foreground)] rounded-full opacity-60 animate-[typingPulse_1.4s_infinite_ease-in-out] [animation-delay:0s]"></span>
<span className="inline-block w-1.5 h-1.5 bg-[var(--app-secondary-foreground)] rounded-full opacity-60 animate-[typingPulse_1.4s_infinite_ease-in-out] [animation-delay:0.2s]"></span>
<span className="inline-block w-1.5 h-1.5 bg-[var(--app-secondary-foreground)] rounded-full opacity-60 animate-[typingPulse_1.4s_infinite_ease-in-out] [animation-delay:0.4s]"></span>
</span>
<MessageContent content={content} onFileClick={onFileClick} />
</div>
{/* Timestamp - temporarily hidden */}
{/* <div
className="text-xs opacity-60"
style={{ color: 'var(--app-secondary-foreground)' }}
>
{new Date(timestamp).toLocaleTimeString()}
</div> */}
</div>
);

View File

@@ -0,0 +1,89 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { MessageContent } from '../MessageContent.js';
interface FileContext {
fileName: string;
filePath: string;
startLine?: number;
endLine?: number;
}
interface UserMessageProps {
content: string;
timestamp: number;
onFileClick?: (path: string) => void;
fileContext?: FileContext;
}
export const UserMessage: React.FC<UserMessageProps> = ({
content,
timestamp: _timestamp,
onFileClick,
fileContext,
}) => {
// Generate display text for file context
const getFileContextDisplay = () => {
if (!fileContext) return null;
const { fileName, startLine, endLine } = fileContext;
if (startLine && endLine) {
return startLine === endLine
? `${fileName}#${startLine}`
: `${fileName}#${startLine}-${endLine}`;
}
return fileName;
};
const fileContextDisplay = getFileContextDisplay();
return (
<div className="flex gap-0 items-start text-left py-2 flex-col relative animate-[fadeIn_0.2s_ease-in]">
<div
className="inline-block my-1 relative whitespace-pre-wrap rounded-md max-w-full overflow-x-auto overflow-y-hidden select-text leading-[1.5]"
style={{
border: '1px solid var(--app-input-border)',
borderRadius: 'var(--corner-radius-medium)',
backgroundColor: 'var(--app-input-background)',
padding: '4px 6px',
color: 'var(--app-primary-foreground)',
}}
>
<MessageContent content={content} onFileClick={onFileClick} />
</div>
{/* File context indicator */}
{fileContextDisplay && (
<div>
<div
role="button"
tabIndex={0}
className="mr"
onClick={() => fileContext && onFileClick?.(fileContext.filePath)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
fileContext && onFileClick?.(fileContext.filePath);
}
}}
style={{ cursor: 'pointer' }}
>
<div
className="gr"
title={fileContextDisplay}
style={{
fontSize: '12px',
color: 'var(--app-secondary-foreground)',
opacity: 0.8,
}}
>
{fileContextDisplay}
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -0,0 +1,31 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
interface WaitingMessageProps {
loadingMessage: string;
}
export const WaitingMessage: React.FC<WaitingMessageProps> = ({
loadingMessage,
}) => (
<div className="flex gap-0 items-start text-left py-2 flex-col relative opacity-85 animate-[fadeIn_0.2s_ease-in]">
<div className="bg-transparent border-0 py-2 flex items-center gap-2">
<span className="inline-flex items-center gap-1 mr-0">
<span className="inline-block w-1.5 h-1.5 bg-[var(--app-secondary-foreground)] rounded-full mr-0 opacity-60 animate-[typingPulse_1.4s_infinite_ease-in-out] [animation-delay:0s]"></span>
<span className="inline-block w-1.5 h-1.5 bg-[var(--app-secondary-foreground)] rounded-full mr-0 opacity-60 animate-[typingPulse_1.4s_infinite_ease-in-out] [animation-delay:0.2s]"></span>
<span className="inline-block w-1.5 h-1.5 bg-[var(--app-secondary-foreground)] rounded-full mr-0 opacity-60 animate-[typingPulse_1.4s_infinite_ease-in-out] [animation-delay:0.4s]"></span>
</span>
<span
className="opacity-70 italic"
style={{ color: 'var(--app-secondary-foreground)' }}
>
{loadingMessage}
</span>
</div>
</div>
);

View File

@@ -0,0 +1,11 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
export { UserMessage } from './UserMessage.js';
export { AssistantMessage } from './AssistantMessage.js';
export { ThinkingMessage } from './ThinkingMessage.js';
export { StreamingMessage } from './StreamingMessage.js';
export { WaitingMessage } from './WaitingMessage.js';