mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 01:07:46 +00:00
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:
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user