utils.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. import { Toast } from '@douyinfe/semi-ui';
  2. import { toastConstants } from '../constants';
  3. import React from 'react';
  4. import { toast } from 'react-toastify';
  5. import { THINK_TAG_REGEX, MESSAGE_ROLES } from '../constants/playground.constants';
  6. import { TABLE_COMPACT_MODES_KEY } from '../constants';
  7. import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile.js';
  8. const HTMLToastContent = ({ htmlContent }) => {
  9. return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
  10. };
  11. export default HTMLToastContent;
  12. export function isAdmin() {
  13. let user = localStorage.getItem('user');
  14. if (!user) return false;
  15. user = JSON.parse(user);
  16. return user.role >= 10;
  17. }
  18. export function isRoot() {
  19. let user = localStorage.getItem('user');
  20. if (!user) return false;
  21. user = JSON.parse(user);
  22. return user.role >= 100;
  23. }
  24. export function getSystemName() {
  25. let system_name = localStorage.getItem('system_name');
  26. if (!system_name) return 'New API';
  27. return system_name;
  28. }
  29. export function getLogo() {
  30. let logo = localStorage.getItem('logo');
  31. if (!logo) return '/logo.png';
  32. return logo;
  33. }
  34. export function getUserIdFromLocalStorage() {
  35. let user = localStorage.getItem('user');
  36. if (!user) return -1;
  37. user = JSON.parse(user);
  38. return user.id;
  39. }
  40. export function getFooterHTML() {
  41. return localStorage.getItem('footer_html');
  42. }
  43. export async function copy(text) {
  44. let okay = true;
  45. try {
  46. await navigator.clipboard.writeText(text);
  47. } catch (e) {
  48. try {
  49. // 构建input 执行 复制命令
  50. var _input = window.document.createElement('input');
  51. _input.value = text;
  52. window.document.body.appendChild(_input);
  53. _input.select();
  54. window.document.execCommand('Copy');
  55. window.document.body.removeChild(_input);
  56. } catch (e) {
  57. okay = false;
  58. console.error(e);
  59. }
  60. }
  61. return okay;
  62. }
  63. // isMobile 函数已移除,请改用 useIsMobile Hook
  64. let showErrorOptions = { autoClose: toastConstants.ERROR_TIMEOUT };
  65. let showWarningOptions = { autoClose: toastConstants.WARNING_TIMEOUT };
  66. let showSuccessOptions = { autoClose: toastConstants.SUCCESS_TIMEOUT };
  67. let showInfoOptions = { autoClose: toastConstants.INFO_TIMEOUT };
  68. let showNoticeOptions = { autoClose: false };
  69. const isMobileScreen = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`).matches;
  70. if (isMobileScreen) {
  71. showErrorOptions.position = 'top-center';
  72. // showErrorOptions.transition = 'flip';
  73. showSuccessOptions.position = 'top-center';
  74. // showSuccessOptions.transition = 'flip';
  75. showInfoOptions.position = 'top-center';
  76. // showInfoOptions.transition = 'flip';
  77. showNoticeOptions.position = 'top-center';
  78. // showNoticeOptions.transition = 'flip';
  79. }
  80. export function showError(error) {
  81. console.error(error);
  82. if (error.message) {
  83. if (error.name === 'AxiosError') {
  84. switch (error.response.status) {
  85. case 401:
  86. // 清除用户状态
  87. localStorage.removeItem('user');
  88. // toast.error('错误:未登录或登录已过期,请重新登录!', showErrorOptions);
  89. window.location.href = '/login?expired=true';
  90. break;
  91. case 429:
  92. Toast.error('错误:请求次数过多,请稍后再试!');
  93. break;
  94. case 500:
  95. Toast.error('错误:服务器内部错误,请联系管理员!');
  96. break;
  97. case 405:
  98. Toast.info('本站仅作演示之用,无服务端!');
  99. break;
  100. default:
  101. Toast.error('错误:' + error.message);
  102. }
  103. return;
  104. }
  105. Toast.error('错误:' + error.message);
  106. } else {
  107. Toast.error('错误:' + error);
  108. }
  109. }
  110. export function showWarning(message) {
  111. Toast.warning(message);
  112. }
  113. export function showSuccess(message) {
  114. Toast.success(message);
  115. }
  116. export function showInfo(message) {
  117. Toast.info(message);
  118. }
  119. export function showNotice(message, isHTML = false) {
  120. if (isHTML) {
  121. toast(<HTMLToastContent htmlContent={message} />, showNoticeOptions);
  122. } else {
  123. Toast.info(message);
  124. }
  125. }
  126. export function openPage(url) {
  127. window.open(url);
  128. }
  129. export function removeTrailingSlash(url) {
  130. if (!url) return '';
  131. if (url.endsWith('/')) {
  132. return url.slice(0, -1);
  133. } else {
  134. return url;
  135. }
  136. }
  137. export function getTodayStartTimestamp() {
  138. var now = new Date();
  139. now.setHours(0, 0, 0, 0);
  140. return Math.floor(now.getTime() / 1000);
  141. }
  142. export function timestamp2string(timestamp) {
  143. let date = new Date(timestamp * 1000);
  144. let year = date.getFullYear().toString();
  145. let month = (date.getMonth() + 1).toString();
  146. let day = date.getDate().toString();
  147. let hour = date.getHours().toString();
  148. let minute = date.getMinutes().toString();
  149. let second = date.getSeconds().toString();
  150. if (month.length === 1) {
  151. month = '0' + month;
  152. }
  153. if (day.length === 1) {
  154. day = '0' + day;
  155. }
  156. if (hour.length === 1) {
  157. hour = '0' + hour;
  158. }
  159. if (minute.length === 1) {
  160. minute = '0' + minute;
  161. }
  162. if (second.length === 1) {
  163. second = '0' + second;
  164. }
  165. return (
  166. year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
  167. );
  168. }
  169. export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
  170. let date = new Date(timestamp * 1000);
  171. // let year = date.getFullYear().toString();
  172. let month = (date.getMonth() + 1).toString();
  173. let day = date.getDate().toString();
  174. let hour = date.getHours().toString();
  175. if (day === '24') {
  176. console.log('timestamp', timestamp);
  177. }
  178. if (month.length === 1) {
  179. month = '0' + month;
  180. }
  181. if (day.length === 1) {
  182. day = '0' + day;
  183. }
  184. if (hour.length === 1) {
  185. hour = '0' + hour;
  186. }
  187. let str = month + '-' + day;
  188. if (dataExportDefaultTime === 'hour') {
  189. str += ' ' + hour + ':00';
  190. } else if (dataExportDefaultTime === 'week') {
  191. let nextWeek = new Date(timestamp * 1000 + 6 * 24 * 60 * 60 * 1000);
  192. let nextMonth = (nextWeek.getMonth() + 1).toString();
  193. let nextDay = nextWeek.getDate().toString();
  194. if (nextMonth.length === 1) {
  195. nextMonth = '0' + nextMonth;
  196. }
  197. if (nextDay.length === 1) {
  198. nextDay = '0' + nextDay;
  199. }
  200. str += ' - ' + nextMonth + '-' + nextDay;
  201. }
  202. return str;
  203. }
  204. export function downloadTextAsFile(text, filename) {
  205. let blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
  206. let url = URL.createObjectURL(blob);
  207. let a = document.createElement('a');
  208. a.href = url;
  209. a.download = filename;
  210. a.click();
  211. }
  212. export const verifyJSON = (str) => {
  213. try {
  214. JSON.parse(str);
  215. } catch (e) {
  216. return false;
  217. }
  218. return true;
  219. };
  220. export function verifyJSONPromise(value) {
  221. try {
  222. JSON.parse(value);
  223. return Promise.resolve();
  224. } catch (e) {
  225. return Promise.reject('不是合法的 JSON 字符串');
  226. }
  227. }
  228. export function shouldShowPrompt(id) {
  229. let prompt = localStorage.getItem(`prompt-${id}`);
  230. return !prompt;
  231. }
  232. export function setPromptShown(id) {
  233. localStorage.setItem(`prompt-${id}`, 'true');
  234. }
  235. /**
  236. * 比较两个对象的属性,找出有变化的属性,并返回包含变化属性信息的数组
  237. * @param {Object} oldObject - 旧对象
  238. * @param {Object} newObject - 新对象
  239. * @return {Array} 包含变化属性信息的数组,每个元素是一个对象,包含 key, oldValue 和 newValue
  240. */
  241. export function compareObjects(oldObject, newObject) {
  242. const changedProperties = [];
  243. // 比较两个对象的属性
  244. for (const key in oldObject) {
  245. if (oldObject.hasOwnProperty(key) && newObject.hasOwnProperty(key)) {
  246. if (oldObject[key] !== newObject[key]) {
  247. changedProperties.push({
  248. key: key,
  249. oldValue: oldObject[key],
  250. newValue: newObject[key],
  251. });
  252. }
  253. }
  254. }
  255. return changedProperties;
  256. }
  257. // playground message
  258. // 生成唯一ID
  259. let messageId = 4;
  260. export const generateMessageId = () => `${messageId++}`;
  261. // 提取消息中的文本内容
  262. export const getTextContent = (message) => {
  263. if (!message || !message.content) return '';
  264. if (Array.isArray(message.content)) {
  265. const textContent = message.content.find(item => item.type === 'text');
  266. return textContent?.text || '';
  267. }
  268. return typeof message.content === 'string' ? message.content : '';
  269. };
  270. // 处理 think 标签
  271. export const processThinkTags = (content, reasoningContent = '') => {
  272. if (!content || !content.includes('<think>')) {
  273. return { content, reasoningContent };
  274. }
  275. const thoughts = [];
  276. const replyParts = [];
  277. let lastIndex = 0;
  278. let match;
  279. THINK_TAG_REGEX.lastIndex = 0;
  280. while ((match = THINK_TAG_REGEX.exec(content)) !== null) {
  281. replyParts.push(content.substring(lastIndex, match.index));
  282. thoughts.push(match[1]);
  283. lastIndex = match.index + match[0].length;
  284. }
  285. replyParts.push(content.substring(lastIndex));
  286. const processedContent = replyParts.join('').replace(/<\/?think>/g, '').trim();
  287. const thoughtsStr = thoughts.join('\n\n---\n\n');
  288. const processedReasoningContent = reasoningContent && thoughtsStr
  289. ? `${reasoningContent}\n\n---\n\n${thoughtsStr}`
  290. : reasoningContent || thoughtsStr;
  291. return {
  292. content: processedContent,
  293. reasoningContent: processedReasoningContent
  294. };
  295. };
  296. // 处理未完成的 think 标签
  297. export const processIncompleteThinkTags = (content, reasoningContent = '') => {
  298. if (!content) return { content: '', reasoningContent };
  299. const lastOpenThinkIndex = content.lastIndexOf('<think>');
  300. if (lastOpenThinkIndex === -1) {
  301. return processThinkTags(content, reasoningContent);
  302. }
  303. const fragmentAfterLastOpen = content.substring(lastOpenThinkIndex);
  304. if (!fragmentAfterLastOpen.includes('</think>')) {
  305. const unclosedThought = fragmentAfterLastOpen.substring('<think>'.length).trim();
  306. const cleanContent = content.substring(0, lastOpenThinkIndex);
  307. const processedReasoningContent = unclosedThought
  308. ? reasoningContent ? `${reasoningContent}\n\n---\n\n${unclosedThought}` : unclosedThought
  309. : reasoningContent;
  310. return processThinkTags(cleanContent, processedReasoningContent);
  311. }
  312. return processThinkTags(content, reasoningContent);
  313. };
  314. // 构建消息内容(包含图片)
  315. export const buildMessageContent = (textContent, imageUrls = [], imageEnabled = false) => {
  316. if (!textContent && (!imageUrls || imageUrls.length === 0)) {
  317. return '';
  318. }
  319. const validImageUrls = imageUrls.filter(url => url && url.trim() !== '');
  320. if (imageEnabled && validImageUrls.length > 0) {
  321. return [
  322. { type: 'text', text: textContent || '' },
  323. ...validImageUrls.map(url => ({
  324. type: 'image_url',
  325. image_url: { url: url.trim() }
  326. }))
  327. ];
  328. }
  329. return textContent || '';
  330. };
  331. // 创建新消息
  332. export const createMessage = (role, content, options = {}) => ({
  333. role,
  334. content,
  335. createAt: Date.now(),
  336. id: generateMessageId(),
  337. ...options
  338. });
  339. // 创建加载中的助手消息
  340. export const createLoadingAssistantMessage = () => createMessage(
  341. MESSAGE_ROLES.ASSISTANT,
  342. '',
  343. {
  344. reasoningContent: '',
  345. isReasoningExpanded: true,
  346. isThinkingComplete: false,
  347. hasAutoCollapsed: false,
  348. status: 'loading'
  349. }
  350. );
  351. // 检查消息是否包含图片
  352. export const hasImageContent = (message) => {
  353. return message &&
  354. Array.isArray(message.content) &&
  355. message.content.some(item => item.type === 'image_url');
  356. };
  357. // 格式化消息用于API请求
  358. export const formatMessageForAPI = (message) => {
  359. if (!message) return null;
  360. return {
  361. role: message.role,
  362. content: message.content
  363. };
  364. };
  365. // 验证消息是否有效
  366. export const isValidMessage = (message) => {
  367. return message &&
  368. message.role &&
  369. (message.content || message.content === '');
  370. };
  371. // 获取最后一条用户消息
  372. export const getLastUserMessage = (messages) => {
  373. if (!Array.isArray(messages)) return null;
  374. for (let i = messages.length - 1; i >= 0; i--) {
  375. if (messages[i].role === MESSAGE_ROLES.USER) {
  376. return messages[i];
  377. }
  378. }
  379. return null;
  380. };
  381. // 获取最后一条助手消息
  382. export const getLastAssistantMessage = (messages) => {
  383. if (!Array.isArray(messages)) return null;
  384. for (let i = messages.length - 1; i >= 0; i--) {
  385. if (messages[i].role === MESSAGE_ROLES.ASSISTANT) {
  386. return messages[i];
  387. }
  388. }
  389. return null;
  390. };
  391. // 计算相对时间(几天前、几小时前等)
  392. export const getRelativeTime = (publishDate) => {
  393. if (!publishDate) return '';
  394. const now = new Date();
  395. const pubDate = new Date(publishDate);
  396. // 如果日期无效,返回原始字符串
  397. if (isNaN(pubDate.getTime())) return publishDate;
  398. const diffMs = now.getTime() - pubDate.getTime();
  399. const diffSeconds = Math.floor(diffMs / 1000);
  400. const diffMinutes = Math.floor(diffSeconds / 60);
  401. const diffHours = Math.floor(diffMinutes / 60);
  402. const diffDays = Math.floor(diffHours / 24);
  403. const diffWeeks = Math.floor(diffDays / 7);
  404. const diffMonths = Math.floor(diffDays / 30);
  405. const diffYears = Math.floor(diffDays / 365);
  406. // 如果是未来时间,显示具体日期
  407. if (diffMs < 0) {
  408. return formatDateString(pubDate);
  409. }
  410. // 根据时间差返回相应的描述
  411. if (diffSeconds < 60) {
  412. return '刚刚';
  413. } else if (diffMinutes < 60) {
  414. return `${diffMinutes} 分钟前`;
  415. } else if (diffHours < 24) {
  416. return `${diffHours} 小时前`;
  417. } else if (diffDays < 7) {
  418. return `${diffDays} 天前`;
  419. } else if (diffWeeks < 4) {
  420. return `${diffWeeks} 周前`;
  421. } else if (diffMonths < 12) {
  422. return `${diffMonths} 个月前`;
  423. } else if (diffYears < 2) {
  424. return '1 年前';
  425. } else {
  426. // 超过2年显示具体日期
  427. return formatDateString(pubDate);
  428. }
  429. };
  430. // 格式化日期字符串
  431. export const formatDateString = (date) => {
  432. const year = date.getFullYear();
  433. const month = String(date.getMonth() + 1).padStart(2, '0');
  434. const day = String(date.getDate()).padStart(2, '0');
  435. return `${year}-${month}-${day}`;
  436. };
  437. // 格式化日期时间字符串(包含时间)
  438. export const formatDateTimeString = (date) => {
  439. const year = date.getFullYear();
  440. const month = String(date.getMonth() + 1).padStart(2, '0');
  441. const day = String(date.getDate()).padStart(2, '0');
  442. const hours = String(date.getHours()).padStart(2, '0');
  443. const minutes = String(date.getMinutes()).padStart(2, '0');
  444. return `${year}-${month}-${day} ${hours}:${minutes}`;
  445. };
  446. function readTableCompactModes() {
  447. try {
  448. const json = localStorage.getItem(TABLE_COMPACT_MODES_KEY);
  449. return json ? JSON.parse(json) : {};
  450. } catch {
  451. return {};
  452. }
  453. }
  454. function writeTableCompactModes(modes) {
  455. try {
  456. localStorage.setItem(TABLE_COMPACT_MODES_KEY, JSON.stringify(modes));
  457. } catch {
  458. // ignore
  459. }
  460. }
  461. export function getTableCompactMode(tableKey = 'global') {
  462. const modes = readTableCompactModes();
  463. return !!modes[tableKey];
  464. }
  465. export function setTableCompactMode(compact, tableKey = 'global') {
  466. const modes = readTableCompactModes();
  467. modes[tableKey] = compact;
  468. writeTableCompactModes(modes);
  469. }