fix(ui): 修复 React key 警告和 forwardRef 问题
- ChatMessage/SessionItem 使用 forwardRef 支持 AnimatePresence - useChat 为 message_received/done 事件生成唯一消息 ID - sessions API 为历史消息添加 ID 字段 - cli 添加 @types/inquirer 依赖
This commit is contained in:
@@ -17,15 +17,16 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-assistant/server": "workspace:*",
|
"@ai-assistant/server": "workspace:*",
|
||||||
"commander": "^12.1.0",
|
"blessed": "^0.1.81",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"ora": "^8.0.1",
|
"commander": "^12.1.0",
|
||||||
"inquirer": "^9.2.12",
|
"inquirer": "^9.2.12",
|
||||||
"blessed": "^0.1.81"
|
"ora": "^8.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/blessed": "^0.1.25",
|
"@types/blessed": "^0.1.25",
|
||||||
"@types/bun": "^1.1.0",
|
"@types/bun": "^1.1.0",
|
||||||
|
"@types/inquirer": "^9.0.9",
|
||||||
"@types/node": "^20.11.0",
|
"@types/node": "^20.11.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -134,8 +134,17 @@ sessionsRouter.get('/:id/messages', async (c) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 为消息添加 ID(Core 的 ModelMessage 格式没有 id 字段)
|
||||||
|
const messagesWithId = sessionData.messages.map(
|
||||||
|
(msg: { role: string; content: unknown }, index: number) => ({
|
||||||
|
...msg,
|
||||||
|
id: `${msg.role}-${id}-${index}`,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return c.json({
|
return c.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: sessionData.messages,
|
data: messagesWithId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import { User, Bot, Copy, Check } from 'lucide-react';
|
import { User, Bot, Copy, Check } from 'lucide-react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { useState } from 'react';
|
import { useState, forwardRef } from 'react';
|
||||||
import { cn } from '../utils/cn';
|
import { cn } from '../utils/cn';
|
||||||
import { fadeInUp, smoothTransition } from '../utils/animations';
|
import { fadeInUp, smoothTransition } from '../utils/animations';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
@@ -14,7 +14,8 @@ interface ChatMessageProps {
|
|||||||
message: Message;
|
message: Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessage({ message }: ChatMessageProps) {
|
export const ChatMessage = forwardRef<HTMLDivElement, ChatMessageProps>(
|
||||||
|
({ message }, ref) => {
|
||||||
const isUser = message.role === 'user';
|
const isUser = message.role === 'user';
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export function ChatMessage({ message }: ChatMessageProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
variants={fadeInUp}
|
variants={fadeInUp}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
@@ -69,7 +71,8 @@ export function ChatMessage({ message }: ChatMessageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
interface StreamingMessageProps {
|
interface StreamingMessageProps {
|
||||||
content: string;
|
content: string;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* 支持响应式:responsive=true 时桌面端固定显示,移动端抽屉式菜单
|
* 支持响应式:responsive=true 时桌面端固定显示,移动端抽屉式菜单
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, forwardRef } from 'react';
|
||||||
import { Plus, MessageSquare, Trash2, Menu, X, MessageCircle } from 'lucide-react';
|
import { Plus, MessageSquare, Trash2, Menu, X, MessageCircle } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@@ -127,9 +127,11 @@ export function Sidebar({
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 会话列表项
|
// 会话列表项 - 使用 forwardRef 支持 AnimatePresence 的 popLayout 模式
|
||||||
const SessionItem = ({ session }: { session: Session }) => (
|
const SessionItem = forwardRef<HTMLDivElement, { session: Session }>(
|
||||||
|
({ session }, ref) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
layout
|
layout
|
||||||
variants={fadeInUp}
|
variants={fadeInUp}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
@@ -161,6 +163,7 @@ export function Sidebar({
|
|||||||
<Trash2 size={14} className="text-gray-400" />
|
<Trash2 size={14} className="text-gray-400" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
|
|||||||
@@ -118,11 +118,11 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
|||||||
|
|
||||||
case 'done':
|
case 'done':
|
||||||
setState((prev) => {
|
setState((prev) => {
|
||||||
const newMessage: Message = message.payload || {
|
const newMessage: Message = {
|
||||||
id: Date.now().toString(),
|
id: message.payload?.id || `assistant-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: prev.streamingContent,
|
content: message.payload?.content || prev.streamingContent,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: message.payload?.timestamp || new Date().toISOString(),
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -134,11 +134,19 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'message_received':
|
case 'message_received':
|
||||||
// 用户消息已确认
|
// 用户消息已确认 - 构建完整的消息对象
|
||||||
setState((prev) => ({
|
setState((prev) => {
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: `user-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
||||||
|
role: 'user',
|
||||||
|
content: message.payload?.content || '',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
messages: [...prev.messages, message.payload],
|
messages: [...prev.messages, userMessage],
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
|
|||||||
Generated
+18
@@ -39,6 +39,9 @@ importers:
|
|||||||
'@types/bun':
|
'@types/bun':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.3.4
|
version: 1.3.4
|
||||||
|
'@types/inquirer':
|
||||||
|
specifier: ^9.0.9
|
||||||
|
version: 9.0.9
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^20.11.0
|
specifier: ^20.11.0
|
||||||
version: 20.19.26
|
version: 20.19.26
|
||||||
@@ -2155,6 +2158,9 @@ packages:
|
|||||||
'@types/hast@3.0.4':
|
'@types/hast@3.0.4':
|
||||||
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||||
|
|
||||||
|
'@types/inquirer@9.0.9':
|
||||||
|
resolution: {integrity: sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw==}
|
||||||
|
|
||||||
'@types/js-yaml@4.0.9':
|
'@types/js-yaml@4.0.9':
|
||||||
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
|
||||||
|
|
||||||
@@ -2190,6 +2196,9 @@ packages:
|
|||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
|
||||||
|
'@types/through@0.0.33':
|
||||||
|
resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==}
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
@@ -6781,6 +6790,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
'@types/inquirer@9.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@types/through': 0.0.33
|
||||||
|
rxjs: 7.8.2
|
||||||
|
|
||||||
'@types/js-yaml@4.0.9': {}
|
'@types/js-yaml@4.0.9': {}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
@@ -6816,6 +6830,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
|
|
||||||
|
'@types/through@0.0.33':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.19.2
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7': {}
|
'@types/trusted-types@2.0.7': {}
|
||||||
|
|
||||||
'@types/unist@2.0.11': {}
|
'@types/unist@2.0.11': {}
|
||||||
|
|||||||
Reference in New Issue
Block a user