feat:替换 Dify 为自建 RAG去实现
1、修复了若干无权限时的失败提示语 2、新增了一个生成后续建议问题的功能 3、重构了知识问答部分的权限管理模块 4、修复了若干渲染不恰当的样式渲染
This commit is contained in:
@@ -13,6 +13,7 @@ interface ChatMessageProps {
|
||||
onFeedback?: (messageId: string, feedback: Feedbacktype) => void;
|
||||
isResponding?: boolean;
|
||||
onRegenerate?: (messageId: string) => void;
|
||||
onSuggestedQuestionClick?: (question: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,7 +23,8 @@ export default function ChatMessage({
|
||||
message,
|
||||
onFeedback,
|
||||
isResponding = false,
|
||||
onRegenerate
|
||||
onRegenerate,
|
||||
onSuggestedQuestionClick,
|
||||
}: ChatMessageProps) {
|
||||
const [feedback, setFeedback] = useState<'like' | 'dislike' | null>(
|
||||
message.feedback?.rating || null
|
||||
@@ -124,28 +126,51 @@ export default function ChatMessage({
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染建议问题
|
||||
* 渲染建议问题(继续探索)
|
||||
*/
|
||||
const renderSuggestedQuestions = () => {
|
||||
if (!suggestedQuestions || suggestedQuestions.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="suggested-questions">
|
||||
<div className="text-sm text-gray-500 mb-2">建议问题:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="suggested-questions" style={{ marginTop: 12, paddingTop: 12, borderTop: '1px solid #f0f0f0' }}>
|
||||
<div style={{ fontSize: 11, color: '#8c8c8c', marginBottom: 8, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<i className="ri-compass-3-line" />
|
||||
<span>继续探索</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{suggestedQuestions.map((question, index) => (
|
||||
<Button
|
||||
<button
|
||||
key={index}
|
||||
size="small"
|
||||
type="dashed"
|
||||
className="question-button text-left"
|
||||
onClick={() => {
|
||||
// 这里可以添加点击建议问题的处理逻辑
|
||||
// console.log('Suggested question clicked:', question);
|
||||
onClick={() => onSuggestedQuestionClick?.(question)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '6px 10px',
|
||||
border: '1px solid #e8e8e8',
|
||||
borderRadius: 6,
|
||||
background: '#fafafa',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontSize: 13,
|
||||
color: '#595959',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#00684a';
|
||||
e.currentTarget.style.color = '#00684a';
|
||||
e.currentTarget.style.background = 'rgba(0,104,74,0.04)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = '#e8e8e8';
|
||||
e.currentTarget.style.color = '#595959';
|
||||
e.currentTarget.style.background = '#fafafa';
|
||||
}}
|
||||
>
|
||||
{question}
|
||||
</Button>
|
||||
<i className="ri-search-line" style={{ color: '#8c8c8c', flexShrink: 0, fontSize: 12 }} />
|
||||
<span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{question}</span>
|
||||
<i className="ri-arrow-right-line" style={{ color: '#bfbfbf', flexShrink: 0, fontSize: 12 }} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,10 +223,14 @@ export default function ChatMessage({
|
||||
<div className="flex items-start gap-2">
|
||||
{/* 消息内容 */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{isAnswer ? renderAnswerContent() : (
|
||||
{isAnswer ? (
|
||||
<>
|
||||
{renderAnswerContent()}
|
||||
{!isResponding && renderSuggestedQuestions()}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<Markdown content={content} />
|
||||
{/* {renderImages(message_files)} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useBoolean, useGetState } from 'ahooks';
|
||||
import { Layout, theme } from 'antd';
|
||||
import { Layout } from 'antd';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import ChatInput from './chat-input';
|
||||
import ChatMessage from './chat-message';
|
||||
@@ -8,6 +8,7 @@ import ChatSidebar, { type ChatSidebarRef } from './sidebar';
|
||||
import type { ChatItem, ConversationItem } from '~/api/dify-chat';
|
||||
import { fetchAppParams, fetchChatList, fetchConversations } from '~/api/dify-chat';
|
||||
import { CHAT_CONFIG } from '../../config/chat';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import useChatMessage from '../../hooks/use-chat-message';
|
||||
import useConversation from '../../hooks/use-conversation';
|
||||
import { useChatApps } from '../../hooks/dify-chat-apps/useChatApps';
|
||||
@@ -28,29 +29,34 @@ interface ChatTheme {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主题token - 避免在SSR环境中调用
|
||||
* 主题配置常量
|
||||
* 避免 SSR 环境下调用 theme.useToken() 导致 CSS-in-JS 注入报错
|
||||
*/
|
||||
function useChatTheme(): ChatTheme {
|
||||
// Ant Design的theme.useToken()必须在组件顶层调用,不能放在useEffect中
|
||||
const antdToken = typeof window !== 'undefined' ? theme.useToken().token : null;
|
||||
|
||||
return {
|
||||
colorBgContainer: antdToken?.colorBgContainer || '#ffffff',
|
||||
borderRadiusLG: antdToken?.borderRadiusLG || 8,
|
||||
};
|
||||
}
|
||||
const CHAT_THEME: ChatTheme = {
|
||||
colorBgContainer: '#ffffff',
|
||||
borderRadiusLG: 8,
|
||||
};
|
||||
|
||||
/**
|
||||
* 主聊天组件
|
||||
* 实现单页面应用模式,参考webapp-conversation的初始化逻辑
|
||||
*/
|
||||
export default function Chat() {
|
||||
// SSR 兼容:Ant Design 的 CSS-in-JS 在 Remix SSR 环境下
|
||||
// hydration 时会因找不到样式容器而报错,需要客户端渲染
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
// 权限检查
|
||||
const { hasPermission: checkPerm } = usePermission();
|
||||
const canChat = checkPerm('dify:chat:use');
|
||||
|
||||
// 侧边栏状态
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
// 获取主题配置,避免SSR错误
|
||||
const { colorBgContainer, borderRadiusLG } = useChatTheme();
|
||||
// 主题配置
|
||||
const { colorBgContainer, borderRadiusLG } = CHAT_THEME;
|
||||
|
||||
// 对话应用管理
|
||||
const {
|
||||
@@ -162,8 +168,11 @@ export default function Chat() {
|
||||
// 应用状态
|
||||
const [appUnavailable, setAppUnavailable] = useState<boolean>(false);
|
||||
const [isUnknownReason, setIsUnknownReason] = useState<boolean>(false);
|
||||
const [conversationPermissionDenied, setConversationPermissionDenied] = useState<boolean>(false);
|
||||
const [inited, setInited] = useState<boolean>(false);
|
||||
const [promptConfig, setPromptConfig] = useState<any>(null);
|
||||
// 防止重复初始化
|
||||
const [initializing, setInitializing] = useState<boolean>(false);
|
||||
|
||||
// 会话状态管理
|
||||
const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false);
|
||||
@@ -507,7 +516,8 @@ export default function Chat() {
|
||||
};
|
||||
|
||||
/**
|
||||
* 组件初始化 - 参考webapp-conversation的逻辑
|
||||
* 组件初始化 - 等待 currentChatApp 就绪后再获取会话列表
|
||||
* 确保按当前应用过滤会话,避免不同应用的会话混在一起
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!hasSetAppConfig) {
|
||||
@@ -516,14 +526,28 @@ export default function Chat() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 必须等 currentChatApp 就绪,否则不知道该获取哪个应用的会话
|
||||
if (!currentChatApp || initializing || inited) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInitializing(true);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// console.log('🚀 开始初始化聊天应用...');
|
||||
console.log('🚀 [Chat] 开始初始化,当前应用:', currentChatApp.app_name, currentChatApp.app_id);
|
||||
|
||||
// 并行获取会话列表和应用参数
|
||||
// 用当前应用的 appId 获取会话列表(失败时降级为空列表,不阻塞初始化)
|
||||
const [conversationData, appParams] = await Promise.all([
|
||||
fetchConversations(),
|
||||
fetchAppParams()
|
||||
fetchConversations(currentChatApp.app_id).catch(err => {
|
||||
console.warn('⚠️ [Chat] 获取会话列表失败(权限不足或网络问题),降级为空列表:', err.message);
|
||||
setConversationPermissionDenied(true);
|
||||
return { data: [] };
|
||||
}),
|
||||
fetchAppParams().catch(err => {
|
||||
console.warn('⚠️ [Chat] 获取应用参数失败,使用默认值:', err.message);
|
||||
return { data: { user_input_form: [], opening_statement: '' } };
|
||||
}),
|
||||
]);
|
||||
|
||||
console.log('📋 [Chat] 获取到的数据:', { conversationData, appParams });
|
||||
@@ -532,13 +556,8 @@ export default function Chat() {
|
||||
const conversations = (conversationData as any).data || [];
|
||||
console.log('📋 [Chat] 会话列表:', conversations);
|
||||
|
||||
if ((conversationData as any).error) {
|
||||
console.error('❌ [Chat] 获取会话列表失败:', (conversationData as any).error);
|
||||
throw new Error((conversationData as any).error);
|
||||
}
|
||||
|
||||
// 处理当前会话ID
|
||||
const _conversationId = getConversationIdFromStorage(CHAT_CONFIG.APP_ID);
|
||||
const _conversationId = getConversationIdFromStorage(currentChatApp.app_id);
|
||||
const isNotNewConversation = conversations.some((item: ConversationItem) => item.id === _conversationId);
|
||||
|
||||
console.log('💾 [Chat] 初始化 - 本地存储的会话ID:', {
|
||||
@@ -568,15 +587,15 @@ export default function Chat() {
|
||||
// 如果存在有效的会话ID,则设置为当前会话
|
||||
if (isNotNewConversation) {
|
||||
console.log('🎯 [Chat] 初始化 - 设置当前会话ID:', _conversationId);
|
||||
setCurrConversationId(_conversationId, CHAT_CONFIG.APP_ID, false);
|
||||
setCurrConversationId(_conversationId, currentChatApp.app_id, false);
|
||||
} else {
|
||||
// 如果localStorage为空或会话不存在,自动创建新会话
|
||||
console.log('🆕 [Chat] 初始化 - localStorage为空或会话不存在,创建新会话');
|
||||
setCurrConversationId('-1', CHAT_CONFIG.APP_ID, false);
|
||||
setCurrConversationId('-1', currentChatApp.app_id, false);
|
||||
}
|
||||
|
||||
setInited(true);
|
||||
// console.log('✅ 聊天应用初始化完成');
|
||||
console.log('✅ [Chat] 聊天应用初始化完成');
|
||||
} catch (e: any) {
|
||||
console.error('❌ 初始化失败:', e);
|
||||
if (e.status === 404) {
|
||||
@@ -587,7 +606,7 @@ export default function Chat() {
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
}, [currentChatApp]);
|
||||
|
||||
// 监听会话切换
|
||||
useEffect(() => {
|
||||
@@ -663,6 +682,18 @@ export default function Chat() {
|
||||
const conversationName = currConversationInfo?.name || '新对话';
|
||||
const conversationIntroduction = currConversationInfo?.introduction || '';
|
||||
|
||||
// SSR 兼容:等客户端挂载后再渲染 Ant Design 组件
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
||||
<p className="text-gray-600">正在加载...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout style={{ height: '100%', display: 'flex', flexDirection: 'row', position: 'relative' }}>
|
||||
{/* 移动端遮罩层 - 点击可收起侧边栏 */}
|
||||
@@ -704,6 +735,7 @@ export default function Chat() {
|
||||
loadingChatApps={loadingChatApps}
|
||||
currentChatApp={currentChatApp}
|
||||
onChatAppChange={handleChatAppChange}
|
||||
conversationReadOnly={conversationPermissionDenied}
|
||||
/>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
@@ -738,6 +770,7 @@ export default function Chat() {
|
||||
message={item}
|
||||
isResponding={isResponding && item.id === chatList[chatList.length - 1]?.id}
|
||||
onFeedback={handleFeedback}
|
||||
onSuggestedQuestionClick={(question) => handleSendMessage(question)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -747,8 +780,8 @@ export default function Chat() {
|
||||
<div className="flex-shrink-0 bg-white">
|
||||
<ChatInput
|
||||
onSendMessage={handleSendMessage}
|
||||
disabled={isResponding}
|
||||
placeholder="有什么我能帮您的吗?"
|
||||
disabled={isResponding || !canChat}
|
||||
placeholder={canChat ? "有什么我能帮您的吗?" : "您没有发送消息的权限"}
|
||||
onStop={stopResponding}
|
||||
isResponding={isResponding}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Button, Dropdown, Input, Layout, Menu, Modal, Tooltip, message, theme,
|
||||
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
|
||||
import type { ChatApp } from '~/api/dify-chat-apps/types';
|
||||
import type { ConversationItem } from '~/api/dify-chat';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import { deleteConversation, renameConversation } from '~/api/dify-chat';
|
||||
import '../../styles/components/chat-with-llm/sidebar.css';
|
||||
|
||||
@@ -32,6 +33,8 @@ interface ChatSidebarProps {
|
||||
currentChatApp: ChatApp | null;
|
||||
onChatAppChange: (appId: string) => void;
|
||||
onConversationRenamed?: (conversationId: string, newName: string) => void;
|
||||
/** 会话列表权限被拒绝时,隐藏重命名/删除按钮 */
|
||||
conversationReadOnly?: boolean;
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法接口
|
||||
@@ -55,7 +58,10 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
|
||||
onNewConversation,
|
||||
onConversationDeleted,
|
||||
onConversationRenamed,
|
||||
conversationReadOnly = false,
|
||||
}, ref) => {
|
||||
const { hasPermission } = usePermission();
|
||||
const canDeleteConversation = hasPermission('dify:conversation:delete');
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [renameModalVisible, setRenameModalVisible] = useState(false);
|
||||
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
|
||||
@@ -208,24 +214,24 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
|
||||
<span className="truncate flex-1" title={conv.name}>
|
||||
{conv.name}
|
||||
</span>
|
||||
{!collapsed && (
|
||||
{!collapsed && (!conversationReadOnly || canDeleteConversation) && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
...(!conversationReadOnly ? [{
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
label: '重命名',
|
||||
onClick: () => handleRename(conv),
|
||||
},
|
||||
{
|
||||
}] : []),
|
||||
...(canDeleteConversation ? [{
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
label: '删除',
|
||||
danger: true,
|
||||
onClick: () => handleDeleteClick(conv),
|
||||
},
|
||||
],
|
||||
}] : []),
|
||||
].filter(Boolean),
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="bottomRight"
|
||||
|
||||
Reference in New Issue
Block a user