From 69c001bf84eb733f554c3874e2d12ac86a48fed0 Mon Sep 17 00:00:00 2001 From: zhaoying Date: Thu, 19 Mar 2026 20:41:54 +0800 Subject: [PATCH] feat(web): memory chat ui upgrade --- .../images/conversation/conversation.svg | 13 +- .../images/conversation/deepThinking.svg | 33 ++- .../images/conversation/normalReply.svg | 28 ++ .../assets/images/conversation/quickReply.svg | 28 ++ .../assets/images/conversation/redbear.png | Bin 0 -> 1952 bytes web/src/components/Chat/ChatInput.tsx | 7 +- web/src/components/Chat/index.tsx | 9 +- web/src/components/Chat/types.ts | 1 + web/src/components/RbCard/ResultCard.tsx | 80 ++++++ web/src/i18n/en.ts | 2 + web/src/i18n/zh.ts | 2 + web/src/views/Conversation/index.tsx | 43 +-- .../views/MemoryConversation/index.module.css | 11 + web/src/views/MemoryConversation/index.tsx | 253 +++++++++--------- .../components/Result.tsx | 4 +- 15 files changed, 344 insertions(+), 170 deletions(-) create mode 100644 web/src/assets/images/conversation/normalReply.svg create mode 100644 web/src/assets/images/conversation/quickReply.svg create mode 100644 web/src/assets/images/conversation/redbear.png create mode 100644 web/src/components/RbCard/ResultCard.tsx create mode 100644 web/src/views/MemoryConversation/index.module.css diff --git a/web/src/assets/images/conversation/conversation.svg b/web/src/assets/images/conversation/conversation.svg index 2ebc02fb..a21f34bc 100644 --- a/web/src/assets/images/conversation/conversation.svg +++ b/web/src/assets/images/conversation/conversation.svg @@ -1,11 +1,12 @@ - + 对话 - - - - - + + + + + + diff --git a/web/src/assets/images/conversation/deepThinking.svg b/web/src/assets/images/conversation/deepThinking.svg index b7658bf4..58ad411f 100644 --- a/web/src/assets/images/conversation/deepThinking.svg +++ b/web/src/assets/images/conversation/deepThinking.svg @@ -1,16 +1,29 @@ 深度思考 - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/src/assets/images/conversation/normalReply.svg b/web/src/assets/images/conversation/normalReply.svg new file mode 100644 index 00000000..19b8c28d --- /dev/null +++ b/web/src/assets/images/conversation/normalReply.svg @@ -0,0 +1,28 @@ + + + 正常 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/conversation/quickReply.svg b/web/src/assets/images/conversation/quickReply.svg new file mode 100644 index 00000000..9a90ef1c --- /dev/null +++ b/web/src/assets/images/conversation/quickReply.svg @@ -0,0 +1,28 @@ + + + 快速回复 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/src/assets/images/conversation/redbear.png b/web/src/assets/images/conversation/redbear.png new file mode 100644 index 0000000000000000000000000000000000000000..8fd5e2f6b148263f6ff1a5b4e8be9ae854957eab GIT binary patch literal 1952 zcmV;R2VeM!P)og@BJ3 z0b2=LP<%BsrG>Qeh!7I6g#uF0_S%Y+R{;wxmL9bCIkW8^_t@RrEiD-Rl1c9FoB7Sm z_sz^V`|b7V^Ish&6y=^;LdF5TSf?nNHL@&Nk>fb`BZ(Y8F_9Bra4`H*2P-Ur?^z5Z zOmH}S4_m>gPfPRE5i->;%Zq9iWx^>@yw|efG&8d=2(GA=FIZvd!C-&514o80EE-k zI$55J{0Xd>0S1*@FxrTAIXFQ^EWe^W2iO5q-GuyneCW^z3>^Tbuj3f6(m_eSCOXBP zH(+=KNlvlk2$4Ii=!WR{PS^OlAwvcNLogn|CNH_*;>5ZWB^RwYQA0>d zj01i`=EW1-$V&|nzpXuOyhbc5Tq+dF$+)|qDDz{(hmg0-gbGBdiDCJ+-ZDN!41c-A zOU{|u0)^!5SO*m4eItRr5^3dF;jdmdzKaI3!l?ri37F(Tj-CS}4VbCq5^Hak@frXxCtM5%O9O>p@e-{jk=wf2OX$EbKxH)c#4fnmOVU--}jyBuwEil1z8|h??Y@0WS^qMJQLmVI^vZkf$cB zKp+NUX}G__;^6Lv%M5dgFWWL+gd~9jt3d<6RXNLntf>0w5BU@q4qg`AiS%CiS~ z>3-?@3gc(|4d*n6QN|Ot+K7qRI9Ca}yv~SS`wh&x&9Vc~{>}_vf#q(W3cLeW%V`i6 zYdoDi;YoK)R_IY6?YlOIym}ajjXIF$gH$PE&cS&m*>FIJNZ!h>EuLl)=c%w_T4=zT zLN65*P}jiZCYY3SxTcv z?W?b;bI4MVdjoXXog93#b14v|0G-pk~y?;(1n^R*J8M(v^2(nma?)?4w+$~ z4s9n%Y>Cc^o4mjj(JI417OFnulZ-4d;#m&p?x%L>=lO3dF_C zizmR$+YKFH81Fbe(9ikKDzjOXu;tZNyezddiT|82gBlH-LmuQcXs1;j$b&ldcOISnsis z!|D^d27%9Nb7wFRi{Y$4*`cjEIUwZPS%vDFT52`|)z!2yCr2gxY&NQ^{dONmbM+3f z`F23IZ2gJG8{4<r2i_3>tHXWE7y90@&KTHIyg3Nk;VQlDJYNnfuOX+8;*3wB+ zX3~Y(OX&Osi|Mo(S#9)_EIn2-j5svW#$~SCGmNhLawBbTZ;xv@ z@*pqD*f9Qd0_WvzU9F(EAb8kLe0rr(^qqHC(29zK>Iw&;l}5tP_#4h4kG)e@*bAZU z=+>J`j^pkIkmcCIcI%ovSyri=SC4HDMtnUz-itkJA?0yG0u|iEi``;qnYkW+cg(Wh zA-JWzX!ve;T|2zjUxLGKi~(iaddTRYgXK$fA5wbV5qMb*JF>o;IRfw7_`C$CkbKMk zmhk96egK@1Z&-R$&xm! = ({ return (
- {previewFileList.length > 0 &&
@@ -142,7 +142,8 @@ const ChatInput: FC = ({ ) })} -
} +
+
} {/* Message input form */}
diff --git a/web/src/components/Chat/index.tsx b/web/src/components/Chat/index.tsx index 9a60918a..eccd422f 100644 --- a/web/src/components/Chat/index.tsx +++ b/web/src/components/Chat/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2025-12-10 16:46:09 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-06 21:05:09 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-19 14:57:56 */ import { type FC } from 'react' import ChatInput from './ChatInput' @@ -25,10 +25,11 @@ const Chat: FC = ({ labelFormat, errorDesc, fileList, - fileChange + fileChange, + className }) => { return ( -
+
{/* Chat content display area */} void; + className?: string; } /** diff --git a/web/src/components/RbCard/ResultCard.tsx b/web/src/components/RbCard/ResultCard.tsx new file mode 100644 index 00000000..495148f9 --- /dev/null +++ b/web/src/components/RbCard/ResultCard.tsx @@ -0,0 +1,80 @@ +/* + * @Author: ZhaoYing + * @Date: 2026-02-03 17:30:51 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-19 15:44:02 + */ +/** + * ResultCard Component + * Collapsible card wrapper for configuration sections + */ + +import { type FC, type ReactNode } from 'react' +import clsx from 'clsx'; +import { Flex, Space, Tooltip } from 'antd'; + +import RbCard from '@/components/RbCard/Card' + +/** + * Component props + */ +interface ResultCardProps { + title: string | ReactNode; + subTitle?: string | ReactNode; + children: ReactNode; + expanded?: boolean; + handleExpand?: () => void; + className?: string; + headerClassName?: string; + bodyClassName?: string; + extra?: ReactNode; + isMiSans?: boolean; +} + +const ResultCard: FC = ({ + title, + subTitle, + children, + expanded, + handleExpand, + extra, + className, + headerClassName, + bodyClassName, + isMiSans = true, +}) => { + return ( + + + {title} + {subTitle && +
+
} +
+ + {extra} + {handleExpand &&
} +
+
} + headerType="borderless" + headerClassName={headerClassName ?? "rb:min-h-[40px]! rb:text-[#212332]! rb:text-[14px]!"} + bodyClassName={bodyClassName ?? "rb:py-0! rb:px-3!"} + className={className ?? "rb:bg-[#F6F6F6]!"} + > + {((expanded && handleExpand) || (!handleExpand))&& children} +
+ ) +} + +export default ResultCard \ No newline at end of file diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index f8f9f59c..457b318c 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1714,6 +1714,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re fileUrl: 'File URL', addRemoteFile: 'Add Remote File', variableConfig: 'Variable Configuration', + + chatTitle: 'Red Bear Space', }, login: { title: 'Red Bear Memory Science', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index ab0896cf..7a0ca153 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1710,6 +1710,8 @@ export const zh = { fileUrl: '文件链接', addRemoteFile: '添加远程文件', variableConfig: '变量配置', + + chatTitle: '记忆熊空间', }, login: { title: '红熊记忆科学', diff --git a/web/src/views/Conversation/index.tsx b/web/src/views/Conversation/index.tsx index 8a67b3ae..ff04d56b 100644 --- a/web/src/views/Conversation/index.tsx +++ b/web/src/views/Conversation/index.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 16:58:03 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-04 12:10:44 + * @Last Modified time: 2026-03-19 17:19:20 */ /** * Conversation Page @@ -24,7 +24,6 @@ import type { HistoryItem, QueryParams, UploadFileListModalRef } from './types' import Empty from '@/components/Empty' import { formatDateTime } from '@/utils/format'; import { randomString } from '@/utils/common' -import BgImg from '@/assets/images/conversation/bg.png' import ChatEmpty from '@/assets/images/empty/chatEmpty.png' import Chat from '@/components/Chat' import type { ChatItem } from '@/components/Chat/types' @@ -345,20 +344,26 @@ const Conversation: FC = () => { return ( -
-
+ +
+
{t('memoryConversation.chatTitle')}
+
+ + handleChangeHistory(null)} >
{t('memoryConversation.startANewConversation')} -
+ {historyList.length > 0 &&
{ {Object.entries(groupHistoryList).map(([date, items]) => (
{date.replace(/\u200e|\u200f/g, '')}
- {items.map(item => ( -
-
handleChangeHistory(item.id)} - > - {item.title} -
-
- ))} + + + {items.map(item => ( +
+
handleChangeHistory(item.id)} + > + {item.title} +
+
+ ))} +
))}
} -
diff --git a/web/src/views/MemoryConversation/index.module.css b/web/src/views/MemoryConversation/index.module.css new file mode 100644 index 00000000..32522f65 --- /dev/null +++ b/web/src/views/MemoryConversation/index.module.css @@ -0,0 +1,11 @@ +.segmented { + background: #F6F6F6; + border: 1px solid #EBEBEB; +} +.segmented:global(.ant-segmented .ant-segmented-item-label) { + min-height: 24px; + line-height: 24px; + padding: 0 8px; + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/web/src/views/MemoryConversation/index.tsx b/web/src/views/MemoryConversation/index.tsx index e0797b90..5c306428 100644 --- a/web/src/views/MemoryConversation/index.tsx +++ b/web/src/views/MemoryConversation/index.tsx @@ -1,8 +1,8 @@ /* * @Author: ZhaoYing * @Date: 2026-02-03 17:09:03 - * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-02-03 17:09:03 + * @Last Modified by: ZhaoYing + * @Last Modified time: 2026-03-19 16:00:39 */ /** * Memory Conversation Page @@ -12,48 +12,39 @@ import { type FC, type ReactNode, useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' -import { Col, Row, App, Skeleton, Space, Select, Flex } from 'antd' +import { Col, Row, App, Skeleton, Select, Segmented, Tooltip, Flex } from 'antd' import dayjs from 'dayjs' import type { AnyObject } from 'antd/es/_util/type'; -import clsx from 'clsx' import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg' import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.png' -import Card from './components/Card' import { readService, getUserMemoryList } from '@/api/memory' import Empty from '@/components/Empty' import Markdown from '@/components/Markdown' import type { Data } from '@/views/UserMemory/types' import Chat from '@/components/Chat' -import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg' -import OnlineIcon from '@/assets/images/conversation/online.svg' -import DeepThinkingIcon from '@/assets/images/conversation/deepThinking.svg' -import ButtonCheckbox from '@/components/ButtonCheckbox' -import DeepThinkingCheckedIcon from '@/assets/images/conversation/deepThinkingChecked.svg' -import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg' -import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg' import type { ChatItem } from '@/components/Chat/types' +import RbCard from '@/components/RbCard/Card'; +import styles from './index.module.css' +import ResultCard from '@/components/RbCard/ResultCard' /** Search mode configuration */ const searchSwitchList = [ { - icon: DeepThinkingIcon, - checkedIcon: DeepThinkingCheckedIcon, + icon:
, value: '0', - label: 'deepThinking' + key: 'deepThinking' }, { - icon: MemoryFunctionIcon, - checkedIcon: MemoryFunctionCheckedIcon, + icon:
, value: '1', - label: 'normalReply' + key: 'normalReply' }, { - icon: OnlineIcon, - checkedIcon: OnlineCheckedIcon, + icon:
, value: '2', - label: 'quickReply' + key: 'quickReply' }, ] @@ -105,7 +96,7 @@ export interface LogItem { * Content wrapper component for analysis items */ const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => ( -
+
{children}
) @@ -120,6 +111,7 @@ const MemoryConversation: FC = () => { const [userList, setUserList] = useState([]) const [search_switch, setSearchSwitch] = useState('0') const [msg, setMsg] = useState('') + const [expandedLogs, setExpandedLogs] = useState>({}) /** Load user list on mount */ useEffect(() => { @@ -139,6 +131,7 @@ const MemoryConversation: FC = () => { } setChatData(prev => [...prev, { content: msg, created_at: new Date().getTime(), role: 'user' }]) setLoading(true) + setExpandedLogs({}) readService({ message: msg, end_user_id: userId, @@ -149,6 +142,7 @@ const MemoryConversation: FC = () => { const response = res as { answer: string; intermediate_outputs: LogItem[] } setChatData(prev => [...prev, { content: response.answer || '-', created_at: new Date().getTime(), role: 'assistant' }]) setLogs(response.intermediate_outputs) + setExpandedLogs(Object.fromEntries(response.intermediate_outputs.map((_, i) => [i, true]))) }) .finally(() => { setLoading(false) @@ -175,46 +169,51 @@ const MemoryConversation: FC = () => { placeholder={t('memoryConversation.searchPlaceholder')} style={{ width: '100%', marginBottom: '16px' }} onChange={setUserId} + variant="borderless" + className="rb:bg-white rb:rounded-lg" /> - + - } - contentClassName='rb:h-[calc(100vh-362px)]' + className="rb:pt-0!" + contentClassName='rb:h-[calc(100%-144px)]' data={chatData} onChange={setMsg} onSend={handleSend} loading={loading} labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')} > - - {searchSwitchList.map(item => ( - handleChange(item.value)} - > - {t(`memoryConversation.${item.label}`)} - - ))} - + ({ + ...item, + icon: {item.icon} + }))} + shape="round" + className={styles.segmented} + onChange={handleChange} + /> - + - {loading ? @@ -226,98 +225,98 @@ const MemoryConversation: FC = () => { subTitle={t('memoryConversation.memoryConversationAnalysisEmptySubTitle')} size={[270, 170]} /> - : + : {logs.map((log, logIndex) => ( -
setExpandedLogs(prev => ({ ...prev, [logIndex]: !prev[logIndex] }))} + extra={log.type === 'verification' &&
{log.result}
} > -
{log.title}
- {log.type === 'problem_split' && Array.isArray(log.data) && log.data.length > 0 - ? - {log.data.map(vo => ( - - <> -
{vo.id}. {vo.question}
-
{vo.reason}
- -
- ))} -
- : log.type === 'problem_extension' && log.data && Object.keys(log.data).length > 0 - ? - {Object.keys(log.data).map((key: string) => ( - - <> -
{key}
- {(log.data as Record)[key].map((item, index) => ( -
{item}
+ {log.type === 'problem_split' && Array.isArray(log.data) && log.data.length > 0 + ? + {log.data.map(vo => ( + + <> +
{vo.id}. {vo.question}
+
{vo.reason}
+ +
+ ))} +
+ : log.type === 'problem_extension' && log.data && Object.keys(log.data).length > 0 + ? + {Object.keys(log.data).map((key: string) => ( + + <> +
{key}
+ {(log.data as Record)[key].map((item, index) => ( +
{item}
+ ))} + +
+ ))} +
+ : log.type === 'search_result' && log.raw_results + ? +
{log.query}
+
+ {typeof log.raw_results === 'string' + ? + : <> + {log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string }, index: number) => ( +
{item.statement}
))} - - - ))} - - : log.type === 'search_result' && log.raw_results - ? -
{log.query}
-
- {typeof log.raw_results === 'string' - ? - : <> - {log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string }, index: number) => ( -
{item.statement}
- ))} - {log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string }, index: number) => ( -
{item.content}
- ))} - - } -
-
- : log.type === 'retrieval_summary' && log.summary - ?
{log.summary}
- : log.type === 'verification' - ? -
{log.query}
-
{log.reason}
-
{log.result}
+ {log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string }, index: number) => ( +
{item.content}
+ ))} + + } +
- : log.type === 'output_type' - ? + : log.type === 'retrieval_summary' && log.summary + ? +
{log.summary}
+
+ : log.type === 'verification' + ? +
{log.query}
+
{log.reason}
+
{log.result}
+
+ : log.type === 'output_type' + ? +
{log.query}
+
{log.summary}
+
+ : log.type === 'input_summary' && log.raw_results + ?
{log.query}
-
{log.summary}
+
{log.summary}
+
+ {typeof log.raw_results === 'string' + ? + : <> + {log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string; } , index: number) => ( +
{item.statement}
+ ))} + {log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string; }, index: number) => ( +
{item.content}
+ ))} + + } +
- : log.type === 'input_summary' && log.raw_results - ? -
{log.query}
-
{log.summary}
-
- {typeof log.raw_results === 'string' - ? - : <> - {log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string; } , index: number) => ( -
{item.statement}
- ))} - {log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string; }, index: number) => ( -
{item.content}
- ))} - - } -
-
- : null - } -
+ : null + } + ))} -
} -
+ + } +
diff --git a/web/src/views/MemoryExtractionEngine/components/Result.tsx b/web/src/views/MemoryExtractionEngine/components/Result.tsx index 3c39bcc1..063d6d6f 100644 --- a/web/src/views/MemoryExtractionEngine/components/Result.tsx +++ b/web/src/views/MemoryExtractionEngine/components/Result.tsx @@ -2,7 +2,7 @@ * @Author: ZhaoYing * @Date: 2026-02-03 17:30:11 * @Last Modified by: ZhaoYing - * @Last Modified time: 2026-03-19 14:22:20 + * @Last Modified time: 2026-03-19 15:38:38 */ /** * Result Component @@ -16,7 +16,6 @@ import { useTranslation } from 'react-i18next' import { Space, Button, Progress, Form, Input, Flex } from 'antd' import { ExclamationCircleFilled, LoadingOutlined } from '@ant-design/icons' import clsx from 'clsx' -import ResultCard from './ResultCard' import type { AnyObject } from 'antd/es/_util/type'; import Card from './Card' @@ -29,6 +28,7 @@ import Markdown from '@/components/Markdown' import { groupDataByType } from '../constant' import Empty from '@/components/Empty' import NoDataIcon from '@/assets/images/empty/noData.png' +import ResultCard from '@/components/RbCard/ResultCard' /** Result metric mapping */ const resultObj = {