feat(web): add markdown syntax highlighting, slash commands, stop button, quote/reply
- PromptEditor with Prism.js syntax highlighting - Slash commands (/run, /status, /images, /list, /help) in chat input - Stop button to cancel ongoing generation - Quote/reply and regenerate/continue actions in chat - MiddlePanel with conversation timestamps and preview - Pipeline progress in chat view - Fix all remaining dark theme classes Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,30 +1,76 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import { renderMarkdown } from '@/lib/markdown';
|
||||
import { RefreshCw, ArrowRight, Quote } from 'lucide-react';
|
||||
import type { Message } from '@/types';
|
||||
|
||||
export function ChatMessage({ message }: { message: Message }) {
|
||||
interface Props {
|
||||
message: Message;
|
||||
onRegenerate?: (msgId: string) => void;
|
||||
onContinue?: (msgId: string) => void;
|
||||
onQuote?: (content: string) => void;
|
||||
}
|
||||
|
||||
export function ChatMessage({ message, onRegenerate, onContinue, onQuote }: Props) {
|
||||
const isUser = message.role === 'user';
|
||||
const isEmpty = !message.content;
|
||||
|
||||
return (
|
||||
<div className={cn('mb-4 flex', isUser ? 'justify-end' : 'justify-start')}>
|
||||
<div
|
||||
className={cn(
|
||||
'max-w-[80%] rounded-xl px-4 py-2.5',
|
||||
isUser
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-white text-zinc-700 border border-zinc-200 shadow-sm'
|
||||
)}
|
||||
>
|
||||
{isEmpty ? (
|
||||
<span className="text-zinc-400 italic text-xs">...</span>
|
||||
) : isUser ? (
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">{message.content}</p>
|
||||
) : (
|
||||
<div
|
||||
className="markdown-body text-sm leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(message.content) }}
|
||||
/>
|
||||
<div className={cn('mb-4 flex group', isUser ? 'justify-end' : 'justify-start')}>
|
||||
<div className="max-w-[80%]">
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-xl px-4 py-2.5',
|
||||
isUser
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-white text-zinc-700 border border-zinc-200 shadow-sm'
|
||||
)}
|
||||
>
|
||||
{isEmpty ? (
|
||||
<span className="text-zinc-400 italic text-xs">...</span>
|
||||
) : isUser ? (
|
||||
<p className="text-sm leading-relaxed whitespace-pre-wrap">{message.content}</p>
|
||||
) : (
|
||||
<div
|
||||
className="markdown-body text-sm leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: renderMarkdown(message.content) }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action buttons - only for assistant messages */}
|
||||
{!isUser && !isEmpty && (
|
||||
<div className="flex gap-1 mt-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{onQuote && (
|
||||
<button
|
||||
onClick={() => onQuote(message.content)}
|
||||
className="flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors"
|
||||
title="引用"
|
||||
>
|
||||
<Quote size={10} />
|
||||
引用
|
||||
</button>
|
||||
)}
|
||||
{onRegenerate && (
|
||||
<button
|
||||
onClick={() => onRegenerate(message.id)}
|
||||
className="flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors"
|
||||
title="重新生成"
|
||||
>
|
||||
<RefreshCw size={10} />
|
||||
重新生成
|
||||
</button>
|
||||
)}
|
||||
{onContinue && (
|
||||
<button
|
||||
onClick={() => onContinue(message.id)}
|
||||
className="flex items-center gap-0.5 px-1.5 py-0.5 rounded text-[10px] text-zinc-400 hover:text-zinc-600 hover:bg-zinc-100 transition-colors"
|
||||
title="继续"
|
||||
>
|
||||
<ArrowRight size={10} />
|
||||
继续
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user