feat: 新增聊天页面,包含消息发送、确认卡片、语音输入及打字机效果组件。
This commit is contained in:
32
src/components/common/Typewriter/Typewriter.tsx
Normal file
32
src/components/common/Typewriter/Typewriter.tsx
Normal 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}</>;
|
||||||
|
};
|
||||||
@@ -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">
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
{msg.role === 'assistant' && index === messages.length - 1 ? (
|
||||||
{msg.content}
|
<Typewriter text={msg.content} speed={20}>
|
||||||
</ReactMarkdown>
|
{(text) => (
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{text}
|
||||||
|
</ReactMarkdown>
|
||||||
|
)}
|
||||||
|
</Typewriter>
|
||||||
|
) : (
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{msg.content}
|
||||||
|
</ReactMarkdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{msg.confirmationCard && (
|
{msg.confirmationCard && (
|
||||||
|
|||||||
Reference in New Issue
Block a user