ChatHeader.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { apiInterceptors, collectApp, unCollectApp } from '@/client/api';
  2. import { ChatContentContext } from '@/pages/chat';
  3. import { ExportOutlined, LoadingOutlined, StarFilled, StarOutlined } from '@ant-design/icons';
  4. import { Spin, Tag, Typography, message } from 'antd';
  5. import copy from 'copy-to-clipboard';
  6. import React, { useContext, useMemo } from 'react';
  7. import { useTranslation } from 'react-i18next';
  8. import { useRequest } from 'ahooks';
  9. import AppDefaultIcon from '../../common/AppDefaultIcon';
  10. const tagColors = ['magenta', 'orange', 'geekblue', 'purple', 'cyan', 'green'];
  11. const ChatHeader: React.FC<{ isScrollToTop: boolean }> = ({ isScrollToTop }) => {
  12. const { appInfo, refreshAppInfo, handleChat, scrollRef, temperatureValue, resourceValue, currentDialogue } =
  13. useContext(ChatContentContext);
  14. const { t } = useTranslation();
  15. const appScene = useMemo(() => {
  16. return appInfo?.team_context?.chat_scene || 'chat_agent';
  17. }, [appInfo]);
  18. // 应用收藏状态
  19. const isCollected = useMemo(() => {
  20. return appInfo?.is_collected === 'true';
  21. }, [appInfo]);
  22. const { run: operate, loading } = useRequest(
  23. async () => {
  24. const [error] = await apiInterceptors(
  25. isCollected ? unCollectApp({ app_code: appInfo.app_code }) : collectApp({ app_code: appInfo.app_code }),
  26. );
  27. if (error) {
  28. return;
  29. }
  30. return await refreshAppInfo();
  31. },
  32. {
  33. manual: true,
  34. },
  35. );
  36. const paramKey: string[] = useMemo(() => {
  37. return appInfo.param_need?.map(i => i.type) || [];
  38. }, [appInfo.param_need]);
  39. if (!Object.keys(appInfo).length) {
  40. return null;
  41. }
  42. const shareApp = async () => {
  43. const success = copy(location.href);
  44. message[success ? 'success' : 'error'](success ? t('copy_success') : t('copy_failed'));
  45. };
  46. // 正常header
  47. const headerContent = () => {
  48. return (
  49. <header className='flex items-center justify-between w-5/6 h-full px-6 bg-[#ffffff99] border dark:bg-[rgba(255,255,255,0.1)] dark:border-[rgba(255,255,255,0.1)] rounded-2xl mx-auto transition-all duration-400 ease-in-out relative'>
  50. <div className='flex items-center'>
  51. <div className='flex w-12 h-12 justify-center items-center rounded-xl mr-4 bg-white'>
  52. <AppDefaultIcon scene={appScene} width={16} height={16} />
  53. </div>
  54. <div className='flex flex-col flex-1'>
  55. <div className='flex items-center text-base text-[#1c2533] dark:text-[rgba(255,255,255,0.85)] font-semibold gap-2'>
  56. <span>{appInfo?.app_name}</span>
  57. <div className='flex gap-1'>
  58. {appInfo?.team_mode && <Tag color='green'>{appInfo?.team_mode}</Tag>}
  59. {appInfo?.team_context?.chat_scene && <Tag color='cyan'>{appInfo?.team_context?.chat_scene}</Tag>}
  60. </div>
  61. </div>
  62. <Typography.Text
  63. className='text-sm text-[#525964] dark:text-[rgba(255,255,255,0.65)] leading-6'
  64. ellipsis={{
  65. tooltip: true,
  66. }}
  67. >
  68. {appInfo?.app_describe}
  69. </Typography.Text>
  70. </div>
  71. </div>
  72. <div className='flex items-center gap-4'>
  73. <div
  74. onClick={async () => {
  75. await operate();
  76. }}
  77. className='flex items-center justify-center w-10 h-10 bg-[#ffffff99] dark:bg-[rgba(255,255,255,0.2)] border border-white dark:border-[rgba(255,255,255,0.2)] rounded-[50%] cursor-pointer'
  78. >
  79. {loading ? (
  80. <Spin spinning={loading} indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
  81. ) : (
  82. <>
  83. {isCollected ? (
  84. <StarFilled style={{ fontSize: 18 }} className='text-yellow-400 cursor-pointer' />
  85. ) : (
  86. <StarOutlined style={{ fontSize: 18, cursor: 'pointer' }} />
  87. )}
  88. </>
  89. )}
  90. </div>
  91. <div
  92. onClick={shareApp}
  93. className='flex items-center justify-center w-10 h-10 bg-[#ffffff99] dark:bg-[rgba(255,255,255,0.2)] border border-white dark:border-[rgba(255,255,255,0.2)] rounded-[50%] cursor-pointer'
  94. >
  95. <ExportOutlined className='text-lg' />
  96. </div>
  97. </div>
  98. {!!appInfo?.recommend_questions?.length && (
  99. <div className='absolute bottom-[-40px] left-0'>
  100. <span className='text-sm text-[#525964] dark:text-[rgba(255,255,255,0.65)] leading-6'>或许你想问:</span>
  101. {appInfo.recommend_questions.map((item, index) => (
  102. <Tag
  103. key={item.id}
  104. color={tagColors[index]}
  105. className='text-xs p-1 px-2 cursor-pointer'
  106. onClick={async () => {
  107. handleChat(item?.question || '', {
  108. app_code: appInfo.app_code,
  109. ...(paramKey.includes('temperature') && { temperature: temperatureValue }),
  110. ...(paramKey.includes('resource') && {
  111. select_param:
  112. typeof resourceValue === 'string'
  113. ? resourceValue
  114. : JSON.stringify(resourceValue) || currentDialogue.select_param,
  115. }),
  116. });
  117. setTimeout(() => {
  118. scrollRef.current?.scrollTo({
  119. top: scrollRef.current?.scrollHeight,
  120. behavior: 'smooth',
  121. });
  122. }, 0);
  123. }}
  124. >
  125. {item.question}
  126. </Tag>
  127. ))}
  128. </div>
  129. )}
  130. </header>
  131. );
  132. };
  133. // 吸顶header
  134. const topHeaderContent = () => {
  135. return (
  136. <header className='flex items-center justify-between w-full h-14 bg-[#ffffffb7] dark:bg-[rgba(41,63,89,0.4)] px-8 transition-all duration-500 ease-in-out'>
  137. <div className='flex items-center'>
  138. <div className='flex items-center justify-center w-8 h-8 rounded-lg mr-2 bg-white'>
  139. <AppDefaultIcon scene={appScene} />
  140. </div>
  141. <div className='flex items-center text-base text-[#1c2533] dark:text-[rgba(255,255,255,0.85)] font-semibold gap-2'>
  142. <span>{appInfo?.app_name}</span>
  143. <div className='flex gap-1'>
  144. {appInfo?.team_mode && <Tag color='green'>{appInfo?.team_mode}</Tag>}
  145. {appInfo?.team_context?.chat_scene && <Tag color='cyan'>{appInfo?.team_context?.chat_scene}</Tag>}
  146. </div>
  147. </div>
  148. </div>
  149. <div
  150. className='flex gap-8'
  151. onClick={async () => {
  152. await operate();
  153. }}
  154. >
  155. {loading ? (
  156. <Spin spinning={loading} indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />} />
  157. ) : (
  158. <>
  159. {isCollected ? (
  160. <StarFilled style={{ fontSize: 18 }} className='text-yellow-400 cursor-pointer' />
  161. ) : (
  162. <StarOutlined style={{ fontSize: 18, cursor: 'pointer' }} />
  163. )}
  164. </>
  165. )}
  166. <ExportOutlined
  167. className='text-lg'
  168. onClick={e => {
  169. e.stopPropagation();
  170. shareApp();
  171. }}
  172. />
  173. </div>
  174. </header>
  175. );
  176. };
  177. return (
  178. <div
  179. className={`h-20 mt-6 ${
  180. appInfo?.recommend_questions && appInfo?.recommend_questions?.length > 0 ? 'mb-6' : ''
  181. } sticky top-0 bg-transparent z-30 transition-all duration-400 ease-in-out`}
  182. >
  183. {isScrollToTop ? topHeaderContent() : headerContent()}
  184. </div>
  185. );
  186. };
  187. export default ChatHeader;