ModelsColumnDefs.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React from 'react';
  16. import { Button, Space, Tag, Typography, Modal, Tooltip } from '@douyinfe/semi-ui';
  17. import {
  18. timestamp2string,
  19. getLobeHubIcon,
  20. stringToColor
  21. } from '../../../helpers';
  22. import { renderLimitedItems, renderDescription } from '../../common/ui/RenderUtils';
  23. const { Text } = Typography;
  24. // Render timestamp
  25. function renderTimestamp(timestamp) {
  26. return <>{timestamp2string(timestamp)}</>;
  27. }
  28. // Render model icon column: prefer model.icon, then fallback to vendor icon
  29. const renderModelIconCol = (record, vendorMap) => {
  30. const iconKey = record?.icon || vendorMap[record?.vendor_id]?.icon;
  31. if (!iconKey) return '-';
  32. return (
  33. <div className="flex items-center justify-center">
  34. {getLobeHubIcon(iconKey, 20)}
  35. </div>
  36. );
  37. };
  38. // Render vendor column with icon
  39. const renderVendorTag = (vendorId, vendorMap, t) => {
  40. if (!vendorId || !vendorMap[vendorId]) return '-';
  41. const v = vendorMap[vendorId];
  42. return (
  43. <Tag
  44. color='white'
  45. shape='circle'
  46. prefixIcon={getLobeHubIcon(v.icon || 'Layers', 14)}
  47. >
  48. {v.name}
  49. </Tag>
  50. );
  51. };
  52. // Render groups (enable_groups)
  53. const renderGroups = (groups) => {
  54. if (!groups || groups.length === 0) return '-';
  55. return renderLimitedItems({
  56. items: groups,
  57. renderItem: (g, idx) => (
  58. <Tag key={idx} size="small" shape='circle' color={stringToColor(g)}>
  59. {g}
  60. </Tag>
  61. ),
  62. });
  63. };
  64. // Render tags
  65. const renderTags = (text) => {
  66. if (!text) return '-';
  67. const tagsArr = text.split(',').filter(Boolean);
  68. return renderLimitedItems({
  69. items: tagsArr,
  70. renderItem: (tag, idx) => (
  71. <Tag key={idx} size="small" shape='circle' color={stringToColor(tag)}>
  72. {tag}
  73. </Tag>
  74. ),
  75. });
  76. };
  77. // Render endpoints (supports object map or legacy array)
  78. const renderEndpoints = (value) => {
  79. try {
  80. const parsed = typeof value === 'string' ? JSON.parse(value) : value;
  81. if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
  82. const keys = Object.keys(parsed || {});
  83. if (keys.length === 0) return '-';
  84. return renderLimitedItems({
  85. items: keys,
  86. renderItem: (key, idx) => (
  87. <Tag key={idx} size="small" shape='circle' color={stringToColor(key)}>
  88. {key}
  89. </Tag>
  90. ),
  91. maxDisplay: 3,
  92. });
  93. }
  94. if (Array.isArray(parsed)) {
  95. if (parsed.length === 0) return '-';
  96. return renderLimitedItems({
  97. items: parsed,
  98. renderItem: (ep, idx) => (
  99. <Tag key={idx} color="white" size="small" shape='circle'>
  100. {ep}
  101. </Tag>
  102. ),
  103. maxDisplay: 3,
  104. });
  105. }
  106. return value || '-';
  107. } catch (_) {
  108. return value || '-';
  109. }
  110. };
  111. // Render quota type
  112. const renderQuotaType = (qt, t) => {
  113. if (qt === 1) {
  114. return (
  115. <Tag color='teal' size='small' shape='circle'>
  116. {t('按次计费')}
  117. </Tag>
  118. );
  119. }
  120. if (qt === 0) {
  121. return (
  122. <Tag color='violet' size='small' shape='circle'>
  123. {t('按量计费')}
  124. </Tag>
  125. );
  126. }
  127. // 未知
  128. return '-';
  129. };
  130. // Render bound channels
  131. const renderBoundChannels = (channels) => {
  132. if (!channels || channels.length === 0) return '-';
  133. return renderLimitedItems({
  134. items: channels,
  135. renderItem: (c, idx) => (
  136. <Tag key={idx} color="white" size="small" shape='circle'>
  137. {c.name}({c.type})
  138. </Tag>
  139. ),
  140. });
  141. };
  142. // Render operations column
  143. const renderOperations = (text, record, setEditingModel, setShowEdit, manageModel, refresh, t) => {
  144. return (
  145. <Space wrap>
  146. {record.status === 1 ? (
  147. <Button
  148. type='danger'
  149. size="small"
  150. onClick={() => manageModel(record.id, 'disable', record)}
  151. >
  152. {t('禁用')}
  153. </Button>
  154. ) : (
  155. <Button
  156. size="small"
  157. onClick={() => manageModel(record.id, 'enable', record)}
  158. >
  159. {t('启用')}
  160. </Button>
  161. )}
  162. <Button
  163. type='tertiary'
  164. size="small"
  165. onClick={() => {
  166. setEditingModel(record);
  167. setShowEdit(true);
  168. }}
  169. >
  170. {t('编辑')}
  171. </Button>
  172. <Button
  173. type='danger'
  174. size="small"
  175. onClick={() => {
  176. Modal.confirm({
  177. title: t('确定是否要删除此模型?'),
  178. content: t('此修改将不可逆'),
  179. onOk: () => {
  180. (async () => {
  181. await manageModel(record.id, 'delete', record);
  182. await refresh();
  183. })();
  184. },
  185. });
  186. }}
  187. >
  188. {t('删除')}
  189. </Button>
  190. </Space>
  191. );
  192. };
  193. // 名称匹配类型渲染(带匹配数量 Tooltip)
  194. const renderNameRule = (rule, record, t) => {
  195. const map = {
  196. 0: { color: 'green', label: t('精确') },
  197. 1: { color: 'blue', label: t('前缀') },
  198. 2: { color: 'orange', label: t('包含') },
  199. 3: { color: 'purple', label: t('后缀') },
  200. };
  201. const cfg = map[rule];
  202. if (!cfg) return '-';
  203. let label = cfg.label;
  204. if (rule !== 0 && record.matched_count) {
  205. label = `${cfg.label} ${record.matched_count}${t('个模型')}`;
  206. }
  207. const tagElement = (
  208. <Tag color={cfg.color} size="small" shape='circle'>
  209. {label}
  210. </Tag>
  211. );
  212. if (rule === 0 || !record.matched_models || record.matched_models.length === 0) {
  213. return tagElement;
  214. }
  215. return (
  216. <Tooltip content={record.matched_models.join(', ')} showArrow>
  217. {tagElement}
  218. </Tooltip>
  219. );
  220. };
  221. export const getModelsColumns = ({
  222. t,
  223. manageModel,
  224. setEditingModel,
  225. setShowEdit,
  226. refresh,
  227. vendorMap,
  228. }) => {
  229. return [
  230. {
  231. title: t('图标'),
  232. dataIndex: 'icon',
  233. width: 70,
  234. align: 'center',
  235. render: (text, record) => renderModelIconCol(record, vendorMap),
  236. },
  237. {
  238. title: t('模型名称'),
  239. dataIndex: 'model_name',
  240. render: (text) => (
  241. <Text copyable onClick={(e) => e.stopPropagation()}>
  242. {text}
  243. </Text>
  244. ),
  245. },
  246. {
  247. title: t('匹配类型'),
  248. dataIndex: 'name_rule',
  249. render: (val, record) => renderNameRule(val, record, t),
  250. },
  251. {
  252. title: t('描述'),
  253. dataIndex: 'description',
  254. render: (text) => renderDescription(text, 200),
  255. },
  256. {
  257. title: t('供应商'),
  258. dataIndex: 'vendor_id',
  259. render: (vendorId, record) => renderVendorTag(vendorId, vendorMap, t),
  260. },
  261. {
  262. title: t('标签'),
  263. dataIndex: 'tags',
  264. render: renderTags,
  265. },
  266. {
  267. title: t('端点'),
  268. dataIndex: 'endpoints',
  269. render: renderEndpoints,
  270. },
  271. {
  272. title: t('已绑定渠道'),
  273. dataIndex: 'bound_channels',
  274. render: renderBoundChannels,
  275. },
  276. {
  277. title: t('可用分组'),
  278. dataIndex: 'enable_groups',
  279. render: renderGroups,
  280. },
  281. {
  282. title: t('计费类型'),
  283. dataIndex: 'quota_type',
  284. render: (qt) => renderQuotaType(qt, t),
  285. },
  286. {
  287. title: t('创建时间'),
  288. dataIndex: 'created_time',
  289. render: (text, record, index) => {
  290. return <div>{renderTimestamp(text)}</div>;
  291. },
  292. },
  293. {
  294. title: t('更新时间'),
  295. dataIndex: 'updated_time',
  296. render: (text, record, index) => {
  297. return <div>{renderTimestamp(text)}</div>;
  298. },
  299. },
  300. {
  301. title: '',
  302. dataIndex: 'operate',
  303. fixed: 'right',
  304. render: (text, record, index) => renderOperations(
  305. text,
  306. record,
  307. setEditingModel,
  308. setShowEdit,
  309. manageModel,
  310. refresh,
  311. t
  312. ),
  313. },
  314. ];
  315. };