feat: 新增聊天页面,包含消息发送、确认卡片、语音输入及打字机效果组件。

This commit is contained in:
2026-01-30 00:00:54 +08:00
parent cd14ef4ec3
commit c555ec1f62
2 changed files with 47 additions and 4 deletions

View File

@@ -0,0 +1,32 @@
import React, { useState, useEffect } from 'react';
interface TypewriterProps {
text: string;
speed?: number;
onComplete?: () => void;
children?: (text: string) => React.ReactNode;
}
export const Typewriter: React.FC<TypewriterProps> = ({ text, speed = 30, onComplete, children }) => {
const [displayedText, setDisplayedText] = useState('');
const [currentIndex, setCurrentIndex] = useState(0);
useEffect(() => {
if (currentIndex < text.length) {
const timeout = setTimeout(() => {
setDisplayedText(prev => prev + text[currentIndex]);
setCurrentIndex(prev => prev + 1);
}, speed);
return () => clearTimeout(timeout);
} else if (onComplete) {
onComplete();
}
}, [currentIndex, text, speed, onComplete]);
if (children) {
return <>{children(displayedText)}</>;
}
return <>{displayedText}</>;
};

View File

@@ -6,6 +6,7 @@ import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import aiService from '../../services/aiService'; import aiService from '../../services/aiService';
import type { AIChatResponse, ConfirmationCard } from '../../types'; import type { AIChatResponse, ConfirmationCard } from '../../types';
import { Typewriter } from '../../components/common/Typewriter/Typewriter';
import './Chat.css'; import './Chat.css';
interface Message { interface Message {
@@ -273,16 +274,26 @@ export default function Chat() {
</div> </div>
) : ( ) : (
<> <>
{messages.map((msg) => ( {messages.map((msg, index) => (
<div key={msg.id} className={`chat-message ${msg.role}`}> <div key={msg.id} className={`chat-message ${msg.role}`}>
<div className="chat-message-avatar"> <div className="chat-message-avatar">
{msg.role === 'assistant' ? '🪙' : '👤'} {msg.role === 'assistant' ? '🪙' : '👤'}
</div> </div>
<div> <div>
<div className="chat-message-content"> <div className="chat-message-content">
{msg.role === 'assistant' && index === messages.length - 1 ? (
<Typewriter text={msg.content} speed={20}>
{(text) => (
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{text}
</ReactMarkdown>
)}
</Typewriter>
) : (
<ReactMarkdown remarkPlugins={[remarkGfm]}> <ReactMarkdown remarkPlugins={[remarkGfm]}>
{msg.content} {msg.content}
</ReactMarkdown> </ReactMarkdown>
)}
</div> </div>
{msg.confirmationCard && ( {msg.confirmationCard && (