Compare commits

...
514 Commits
Author SHA1 Message Date
jialin 57f4ddad26 chore: remove --port backend parameter 2025-06-05 11:29:54 +08:00
jialin de34d0ae00 fix: instances distribute info trigger by click 2025-06-05 11:24:09 +08:00
jialin 4f7037baa0 chore: reset changes on catalog 2025-06-03 18:10:39 +08:00
jialin bd15e9aa6c fix: tips for css files load failed 2025-06-03 16:28:03 +08:00
jialin 907105a492 fix: set default spec to form 2025-06-03 15:35:38 +08:00
gitlawr 0d3da04356 ci: publish v*-dev branches 2025-05-30 13:20:40 +08:00
EVGENY Mandjialin 174f93fe11 Update common.ts 2025-05-17 22:21:40 +08:00
jialin 6340f1fb54 fix: refresh selection after deleting by batch 2025-05-16 16:07:55 +08:00
jialin febdc5b8e2 fix: add delete action for table watch 2025-05-16 11:51:07 +08:00
jialin b5044b8753 fix: clear query do not trigger a search 2025-05-16 10:10:03 +08:00
jialin 00a60b8948 chore: update error components for loading file failed 2025-05-15 22:57:24 +08:00
jialin 17ec70f839 chore: init mask theme 2025-05-15 19:04:40 +08:00
jialin ac03a93c64 chore: add dark mask, remove clear button in audio and image 2025-05-15 18:02:07 +08:00
jialin 5e8692eae5 fix: theme setting miss after refreshing 2025-05-15 11:53:10 +08:00
jialin 2e56e93d83 fix(style): rerank text overlap 2025-05-14 22:34:39 +08:00
jialin 11d0ff2ec3 chore: update iconfont files 2025-05-14 22:19:27 +08:00
jialin 8a59025132 fix: theme setting in login 2025-05-14 22:19:27 +08:00
jialin 5488a8ea82 fix: system theme 2025-05-14 20:13:54 +08:00
jialin 18cb82a0fe chore: preview field use switch 2025-05-14 14:54:32 +08:00
jialin f951358db4 fix: stop thinking when loading end 2025-05-13 20:29:32 +08:00
jialin a23162117c fix: remember me not worke in login 2025-05-13 20:29:32 +08:00
jialin b13e7b89e2 fix(style): downloading popover 2025-05-12 19:16:29 +08:00
jialin 2416fd1c71 fix: add version holder for backend 2025-05-12 15:13:44 +08:00
jialin 2b7eb832ed fix: remove evaluating for files 2025-05-12 15:13:44 +08:00
jialin 1532f47da1 fix: avoid sending all files when evaluating 2025-05-09 19:33:50 +08:00
jialin 2fa20de631 fix: message format with imgs, downloading table style 2025-05-09 15:59:24 +08:00
EVGENY Mandjialin b51e16bba8 Update models.ts 2025-05-09 09:32:14 +08:00
EVGENY Mandjialin 8ae6c4fd7a Update common.ts 2025-05-09 09:32:00 +08:00
jialin 53b51f0740 fix: remove all the slashes at the end 2025-05-08 20:36:34 +08:00
jialin 92b4be90d0 fix: wrong header content-type for curl tts 2025-05-08 18:25:53 +08:00
jialin 80a6658769 chore: update iconfont files 2025-05-08 18:25:53 +08:00
jialin f0f4bd8bf2 style: login modal 2025-05-08 15:22:53 +08:00
jialin e05b323489 chore: add some arguments for mindie 2025-05-08 11:20:25 +08:00
jialin 7f198b64f1 chore: ollama deprecated tips 2025-05-08 11:09:05 +08:00
jialin 1c87fef28d fix: fetch list page more than total page 2025-05-07 16:34:12 +08:00
jialin 816c22979f chore: catalog: scroll to load more data 2025-05-07 16:34:12 +08:00
jialin 42f9a27851 style: remove bg image in login 2025-05-06 20:19:19 +08:00
jialin 6a2d2dfdaf fix: ignore the fields when compare data 2025-05-06 17:42:14 +08:00
jialin efd2088745 style: login page dark style 2025-05-06 17:42:14 +08:00
jialin 56e8e84eaf fix: gpu selector sort 2025-05-06 13:19:06 +08:00
jialin eccebae596 chore: format message content by list 2025-04-30 19:25:06 +08:00
jialin edc36283d9 chore: doc link in compatible message 2025-04-30 19:25:06 +08:00
jialin 2409cfefc9 fix: form values check incorrect 2025-04-30 17:38:15 +08:00
jialin fae73913de style: gauge dark style 2025-04-30 17:38:15 +08:00
jialin 69f398c594 fix: update fields value 2025-04-30 14:56:34 +08:00
jialin fe52238ac2 chore: error handler in stream response 2025-04-30 12:26:37 +08:00
jialin a3501e5417 fix: update form value trigger tips 2025-04-30 11:01:14 +08:00
jialin 52add26905 style: login background 2025-04-29 18:27:38 +08:00
jialin 954b034b8b fix(style): dark style 2025-04-29 18:05:43 +08:00
jialin e5aa5ace15 chore: remove invalid files 2025-04-29 18:05:43 +08:00
EVGENY Mandjialin a6c5ec8581 Update common.ts 2025-04-29 09:35:39 +08:00
jialin beb395ce32 fix(style): global theme settings in login 2025-04-28 18:22:05 +08:00
jialin bcfa80a2d5 chore: remove custom ui 2025-04-28 10:05:06 +08:00
jialin 96e5398ad1 fix(style): dark css vars 2025-04-28 10:05:06 +08:00
jialin 4b748ee43a style: overlay scroller 2025-04-28 10:05:06 +08:00
jialin 838e0f9910 style: code viewer 2025-04-28 10:05:06 +08:00
jialin 548e1378f5 feat: add ThemeTag 2025-04-28 10:05:06 +08:00
jialin fb90d79eb3 chore: custom brand color 2025-04-28 10:05:06 +08:00
jialin 3fb3381a19 style: ajust dark theme 2025-04-28 10:05:06 +08:00
jialin 60d0750695 style: dark theme for scroller 2025-04-28 10:05:06 +08:00
jialin ce9c31f368 chore: dark config 2025-04-28 10:05:06 +08:00
jialin d4d7148f16 fix: create image custom size do not work 2025-04-27 10:51:13 +08:00
jialin b5aba6e007 style: distribution info table word wrap 2025-04-24 23:13:47 +08:00
jialin 2564fa7382 style: distribution info for vllm 2025-04-24 22:47:53 +08:00
jialin c07be7386c revert: remove evaluation loading 2025-04-24 22:17:49 +08:00
jialin c400003ce9 fix: model and file evaluate over close loading 2025-04-24 21:15:42 +08:00
jialin d4ffd8e8e4 fix: do not close loading when evaluating 2025-04-24 20:36:34 +08:00
jialin 9a6b907c11 chore: check default backend deploy from local path 2025-04-24 18:09:54 +08:00
jialin 84d4c7a6f2 chore: do not change the backend in local_path 2025-04-24 17:28:25 +08:00
jialin 0497e2052d fix: audio model use voxbox only 2025-04-24 17:23:25 +08:00
jialin dc8e9e3fef fix: gpus empty 2025-04-24 16:13:58 +08:00
jialin 518a593c2a fix: gpuoptions empty 2025-04-24 16:11:04 +08:00
jialin f7168cc814 fix: quant fp16 first in catalog 2025-04-24 15:49:08 +08:00
jialin 18217dadc3 chore: set mindie to backend in ascend 2025-04-24 15:06:16 +08:00
jialin 559a4969c1 fix: dashboard usage display datas for last 30 days 2025-04-24 12:43:55 +08:00
jialin 7bad6cd20a chore: display the error message of request header 2025-04-23 19:50:58 +08:00
jialin e3987c8aa3 chore: usage data 2025-04-23 16:39:32 +08:00
jialin a46286872d fix: accept image in edit page 2025-04-23 15:51:48 +08:00
jialin 9bb7a125e3 chore: adjust the date range for usage 2025-04-23 14:24:13 +08:00
jialin 3c196abccb style: format large number for chart label 2025-04-23 10:52:00 +08:00
jialin 2e8899dfef chore: remove qwq in ollama options 2025-04-22 19:57:32 +08:00
jialin 8337768c5a fix: check the error type and return as string 2025-04-22 17:03:24 +08:00
jialin 034b371ad4 fix: handle error as string when mismatch the field 2025-04-22 16:51:05 +08:00
jialin 1e68b75c68 chore: stop scrolling when wheeling in chat page 2025-04-22 16:07:44 +08:00
jialin ac1bf7c9bf style: parameter popover maxheight 2025-04-21 20:23:14 +08:00
jialin d60e052f05 fix: do not expand the instance list when request is pending 2025-04-21 18:18:23 +08:00
jialin 43b71c7532 style: bold text 2025-04-21 09:51:56 +08:00
Shion Tanakaandjialin 8ee0a99323 fix: update ja-JP 2025-04-21 09:36:56 +08:00
jialin 50febc342c fix: install command is incorrect after switching 310p 2025-04-18 20:48:24 +08:00
jialin c98667245e chore: update faq link 2025-04-18 20:16:41 +08:00
jialin 76fdf8e501 chore: update storage path holder text 2025-04-18 17:15:18 +08:00
jialin 6270226ead chore: avoid repeat rendering for clicking same image 2025-04-18 15:13:51 +08:00
jialin ed5afff5d7 fix: save mask on initial 2025-04-18 14:40:26 +08:00
jialin 5d8d38bdf0 fix: clear token usage when stoping chat 2025-04-18 11:16:48 +08:00
EVGENY Mandjialin 88af879d31 Update models.ts 2025-04-18 09:30:07 +08:00
EVGENY Mandjialin 91095f14fb Update resources.ts 2025-04-18 09:29:40 +08:00
jialin 65077088b6 fix: handle api error 2025-04-17 17:58:51 +08:00
jialin 47666b7193 fix: handle api error 2025-04-17 17:58:51 +08:00
jialin 353f73ed0b fix: set llamabox to backend for local gguf file 2025-04-17 14:56:49 +08:00
jialin 51ed9dab04 chore: disabled across workers for mindie 2025-04-17 12:34:53 +08:00
jialin c8b0faf05d style: model file source tooltip 2025-04-17 11:55:19 +08:00
jialin 3d7609e3f5 style: chat role 2025-04-16 20:16:26 +08:00
jialin d9611948d7 chore: evaluate error message 2025-04-16 18:02:59 +08:00
jialin 67053db9ba fix(style): api access info modal with 2025-04-16 15:16:31 +08:00
jialin c831c19848 chore: update vllm parameters, do not change backend when file type unknown 2025-04-16 12:34:18 +08:00
jialin ced45bec65 style: overflow layer padding 2025-04-15 11:08:01 +08:00
EVGENY Mandjialin 13ded5595a Update models.ts 2025-04-15 09:28:13 +08:00
EVGENY Mandjialin dfc4a8d713 Update playground.ts 2025-04-15 09:27:41 +08:00
EVGENY Mandjialin 31b9bfa590 Update resources.ts 2025-04-15 09:27:18 +08:00
jialin f93b3c7424 fix(style): see flickering in tooltip overflow dom 2025-04-14 20:39:38 +08:00
jialin 3d32ec7210 chore: allow switch backend between vllm and mindie 2025-04-14 19:29:55 +08:00
jialin b108d4e287 fix(style): tooltip overflow 2025-04-14 16:59:43 +08:00
jialin d8ea86fde1 chore: backend description 2025-04-14 14:08:59 +08:00
jialin 58e979bc45 chore: remove first login ux 2025-04-14 10:37:00 +08:00
jialin d5acf92f8a fix: description consistency 2025-04-14 09:34:16 +08:00
jialin ddd9b9ae97 chore: add parameter --truncation 2025-04-12 11:54:50 +08:00
jialin f77bf37be8 chore: init max tokens for mindie models 2025-04-12 11:48:02 +08:00
jialin fb8fcc5028 fix: set env null after change backend 2025-04-11 20:27:30 +08:00
jialin f80daf6cd4 fix: extra button control 2025-04-11 20:02:39 +08:00
jialin 092aeceb9d chore: clear backend parameters、backend_version、env on change backend 2025-04-11 18:49:06 +08:00
jialin 7542ac9d87 style: hidden expand button when no replicas 2025-04-11 18:49:06 +08:00
jialin b27ebaaccf chore: update iconfont static files 2025-04-11 12:16:35 +08:00
jialin 5b20a7edb8 fix: disabled across_workers for vllm in edit 2025-04-10 17:52:04 +08:00
jialin 48580cb5b0 feat: add api access info modal 2025-04-10 17:30:26 +08:00
jialin 7c79accda7 chore: display warning message after form change in edit model 2025-04-10 12:21:55 +08:00
jialin 1182fdeb43 chore: add backend field in check api 2025-04-09 18:43:42 +08:00
jialin 694ffb0a81 style: add model selection tips in playground 2025-04-09 17:46:18 +08:00
jialin fe8ff2b180 chore: npu ux for add worker 2025-04-09 15:46:30 +08:00
jialin ade1db5215 fix: omit distribution and cpu_offload fields from default spec 2025-04-08 20:58:24 +08:00
jialin 5269cbf862 fix: submit button is incorrect in edit model 2025-04-08 19:44:59 +08:00
jialin d62e43790c chore: extract a model file item 2025-04-08 19:19:41 +08:00
jialin 5766a50bc2 chore: add changes tips 2025-04-08 16:33:19 +08:00
jialin c09503aad8 fix: add backend field in evaluation payload 2025-04-08 14:37:31 +08:00
jialin f6bc183972 fix(style): unify casing of system terms 2025-04-08 13:28:13 +08:00
jialin a087677c6e chore: allow to config distribution in vllm 2025-04-08 09:47:10 +08:00
jialin 529a2ecd81 fix: validate gpu_selector value before check 2025-04-07 20:04:40 +08:00
jialin 3843d1bd3e fix: ollama model change trigger check action 2025-04-07 19:49:37 +08:00
jialin 6be7f6ad68 style: worker filter bar 2025-04-07 19:49:37 +08:00
jialin 30c4fb8032 fix: auto-complete width 2025-04-07 17:10:05 +08:00
jialin a7dee45720 fix: image edit invert mask 2025-04-07 17:10:05 +08:00
jialin f7c597d957 style: shortcut for start to search model 2025-04-07 13:38:53 +08:00
jialin b89e7c6c3d style: form components 2025-04-07 11:51:34 +08:00
jialin 0f7a619a34 chore: add mindie option 2025-04-07 10:31:22 +08:00
jialin 5c1cb120e1 fix: reset some fields for default spec 2025-04-07 10:08:56 +08:00
jialin 9f24e765be fix: check compatiblity after delete selecor 2025-04-05 10:42:24 +08:00
jialin a23f9881ee fix: some fields value are null after select model 2025-04-03 20:42:20 +08:00
EVGENY Mandjialin 80e85a571e Update resources.ts 2025-04-03 20:40:24 +08:00
EVGENY Mandjialin 82a412093e Update models.ts 2025-04-03 20:40:08 +08:00
EVGENY Mandjialin 8986e84dfe Update common.ts 2025-04-03 20:39:53 +08:00
jialin b6136a092d fix: the fields for trigger checking 2025-04-03 18:08:43 +08:00
jialin 939f46c7e6 fix: miss gpu_selector value in catalog 2025-04-03 13:36:09 +08:00
jialin dd8059693d fix: voice list do not be shown 2025-04-02 21:11:50 +08:00
jialin afacd2a8a0 fix: check form data after blur 2025-04-02 20:40:57 +08:00
jialin 4755cf6a25 fix: do not display modelscope source 2025-04-02 17:55:10 +08:00
jialin d3e58c85cf fix: categories value error 2025-04-02 17:55:10 +08:00
jialin 89a6d6a4c1 chore: update add worker command for container 2025-04-02 15:48:46 +08:00
jialin ecbfeb608b fix: can not select voxbox in editing 2025-04-02 14:50:21 +08:00
jialin f8a8314476 chore: check configuration on initial and change values 2025-04-02 14:19:06 +08:00
jialin b1d7bb9fd7 fix: backend is not updated after changing model 2025-04-02 11:24:26 +08:00
EVGENY Mandjialin 28a84a90cf Update resources.ts 2025-04-02 09:22:03 +08:00
EVGENY Mandjialin 067119eac7 Update models.ts 2025-04-02 09:21:27 +08:00
jialin 3fa19cf977 fix: submit form data error 2025-04-01 22:53:25 +08:00
jialin 5cbb5e7d40 style: check compatibility 2025-04-01 20:47:52 +08:00
jialin f5c4749954 chore: check compatibility in edit 2025-04-01 20:47:52 +08:00
jialin a3ee010286 fix: visit auto playground error from model list 2025-04-01 13:58:50 +08:00
jialin 7e8b5f1f63 fix: model filter by worker 2025-04-01 10:56:50 +08:00
jialin eff88528d3 chore: restart on error 2025-03-31 16:21:40 +08:00
jialin f83e49bd3d chore: check compatibility ux 2025-03-31 16:21:40 +08:00
jialin bd6c689d91 chore: compatibility api 2025-03-31 16:21:40 +08:00
jialin f9469a1ea3 fix(style): message toast bgcolor 2025-03-29 21:35:24 +08:00
jialin c0d92fbdd5 chore: add check locales keys 2025-03-28 19:15:57 +08:00
jialin 2079fc1801 fix(style): add worker 2025-03-28 19:15:57 +08:00
jialin 59d01a6bd9 style: table list col span 2025-03-28 19:15:57 +08:00
Shion Tanakaandjialin 4f29fe5040 feat: Add Japanese Language 2025-03-28 15:03:37 +08:00
jialin 455ae8aa05 style(font): font size 14 2025-03-27 18:32:44 +08:00
jialin db417e6a5f style: set theme token 2025-03-27 18:32:44 +08:00
jialin 8d444f24f8 style: table list col span 2025-03-27 16:11:49 +08:00
jialin 618cd667fc feat: row, col component 2025-03-27 16:11:49 +08:00
jialin f4d5419ba1 style: table style 2025-03-27 16:11:49 +08:00
jialin 0d80269e2d fix: windows path style 2025-03-27 12:17:52 +08:00
jialin 861e95673d fix: resource do not display in model files 2025-03-26 19:08:10 +08:00
jialin 997e4a1edd chore: cache resources active tab 2025-03-26 17:31:31 +08:00
jialin bfdf02f065 fix: copy patn button flickering 2025-03-26 17:31:31 +08:00
jialin f658b9d6d9 style: downloading progress 2025-03-26 11:59:49 +08:00
jialin 671fce2961 chore: cleanup file checkbox 2025-03-26 11:59:49 +08:00
EVGENY Mandjialin 93c9ffe109 Update models.ts
update ru
2025-03-26 10:00:04 +08:00
EVGENY Mandjialin 17cd414dde Update resources.ts
update ru
2025-03-26 09:58:33 +08:00
jialin 3bc10291c4 chore: set source as local_path deploy from model files 2025-03-25 21:24:03 +08:00
jialin 8f6c1d0e47 chore: init worker selector 2025-03-25 17:58:01 +08:00
jialin f882fe2c32 fix: deploy from model files source incorrect 2025-03-25 16:39:03 +08:00
jialin b25644c590 chore: worker downloading tooltip 2025-03-25 12:33:29 +08:00
jialin 626e29a001 style: form item style 2025-03-25 12:33:29 +08:00
jialin dca7a6f270 chore: add model file ux 2025-03-25 12:33:29 +08:00
jialin 05ef2b8eaa chore: add model api 2025-03-25 12:33:29 +08:00
jialin 1819649a3a style: backend check info 2025-03-20 12:16:46 +08:00
jialin b8f58b2cc1 fix: can not paste a text in env vars input box 2025-03-19 20:09:43 +08:00
jialin 2ceefab18c feat: download model 2025-03-19 20:09:43 +08:00
jialin 254dd75cd0 style: instance offload info 2025-03-19 11:43:52 +08:00
jialin 6e87d66932 chore: pasted multiple lines in env vars 2025-03-19 11:23:29 +08:00
jialin 3a05d74a36 fix: no undef var error 2025-03-19 11:23:29 +08:00
jialin 4b5f08ccc2 chore: add delete action in table fetch hook 2025-03-19 09:29:31 +08:00
EVGENY Mandjialin cc20050dc8 Update models.ts
update ru
2025-03-18 23:51:40 +08:00
jialin 25cf439f03 chore: extract fetch hooks for common table 2025-03-18 20:35:29 +08:00
EVGENY Mandjialin 2beae9ea5b Update playground.ts
update translation playground.chat.thinking
2025-03-18 18:08:33 +08:00
EVGENY Mandjialin 64a1ab5c60 Update playground.ts
Update RU
2025-03-18 17:19:38 +08:00
jialin 00ef7ab6ca fix: edit mask for undo 2025-03-18 17:15:55 +08:00
jialin 08b3c9d1ac fix: remove TODO tips in locales 2025-03-17 20:25:03 +08:00
jialin 2287196746 chore: load mask by pix 2025-03-17 20:25:03 +08:00
jialin 9ac679d437 fix: block body scroll when view version 2025-03-14 19:40:15 +08:00
jialin db0a5875fa fix: show success message when start model falied 2025-03-14 18:50:45 +08:00
jialin 38ff16f3cd fix(style): always display offloads info 2025-03-14 16:43:36 +08:00
jialin ccbc36f991 fix: do not call models watch after first loading 2025-03-14 16:43:36 +08:00
jialin b47b99bd28 fix: cpu offloads do not display 2025-03-14 10:22:23 +08:00
jialin 55a922427b fix: cot button can not operate 2025-03-13 21:11:53 +08:00
jialin a5525c065c style: chat message render markdown by default 2025-03-13 18:49:20 +08:00
EVGENY Mandjialin 840b3c2a84 Update common.ts
Update RU
2025-03-13 11:11:59 +08:00
EVGENY Mandjialin 3cef5f0bff Update models.ts
Update ru
2025-03-13 11:11:46 +08:00
jialin fa1463939e chore: verfiy docs 2025-03-12 18:47:46 +08:00
jialin b31099c94d chore: update the llamabox backend tips 2025-03-12 18:40:10 +08:00
jialin fc0899d9d3 fix: do not get the backend when local path blur 2025-03-12 17:41:38 +08:00
jialin a7170a4eee chore: merge fetch data request 2025-03-12 16:53:43 +08:00
jialin e04874d074 fix: remove test code 2025-03-12 10:57:29 +08:00
jialin 0fc570b138 chore: the backend matches the model file 2025-03-12 10:43:59 +08:00
jialin 5edbc1a7b5 style: model list buttons 2025-03-12 10:43:59 +08:00
jialin a7cfa05504 fix: markdown word bread 2025-03-10 17:40:55 +08:00
jialin c0988da176 fix: image viewer flick 2025-03-10 17:11:32 +08:00
jialin 12707793b1 chore: display vllm distributed info 2025-03-10 15:11:48 +08:00
jialin 06d6732ab2 docs: update readme.md 2025-03-10 12:33:43 +08:00
jialin ab6e98aae2 chore: press enter to submit chat message 2025-03-09 21:31:58 +08:00
jialin a2ef9d781d chore: display instance resource claim 2025-03-07 17:07:40 +08:00
jialin 106ee43171 fix: base64 convert to file for all image type 2025-03-06 20:27:34 +08:00
jialin 36d626c833 chore: ram vram usage in resource 2025-03-06 19:44:35 +08:00
jialin b0ac85d81d fix: edit image: image field is null 2025-03-06 16:34:55 +08:00
jialin 25d70b7b49 fix: preview field missing in image 2025-03-06 11:55:16 +08:00
jialin 4c5946e8da fix: can be cleared during loading in embedding and rerank 2025-03-06 09:49:07 +08:00
jialin 4de1a38c00 fix: do not init meta data when switch tab 2025-03-05 20:30:53 +08:00
jialin caadc02fb4 chore: reduce re-render times for params in playground 2025-03-05 15:09:47 +08:00
jialin 3e691aa31f fix: default model in embedding page 2025-03-05 14:05:12 +08:00
jialin fd7ce9027a feat: add expand all rows 2025-03-05 14:05:12 +08:00
EVGENY Mandjialin 3fdde5f8c4 Update playground.ts
Upd
2025-03-05 11:25:37 +08:00
jialin 4c5eb721fb chore: update TODO for locales 2025-03-04 19:18:13 +08:00
jialin f3bdb03cae chore: add TODO: Translate me in locales 2025-03-04 18:50:34 +08:00
jialin 9b6112d206 fix: common.ts locales miss comma in ru 2025-03-04 18:07:42 +08:00
EVGENY Mandjialin beab0fa6f6 Rename ru_RU.ts to ru-RU.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin fa60cab587 Create ru_RU.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin 05da6b3c50 Create users.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin 0b12f4e6fd Create usage.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin b9ddf29289 Create shortcuts.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin 10e47b8305 Create resources.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin 0e1c9487f0 Create playground.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin 60f569dbf9 Create models.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin bdb803db21 Create menu.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin 634fab4d9f Create dashboard.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin 34a14865eb Create common.ts 2025-03-04 18:01:29 +08:00
rjohny55andjialin abcd3a30ce Create apikeys.ts
add ru_RU
2025-03-04 18:01:29 +08:00
jialin 9f40bf6f5d chore: generate invertion mask in image editor 2025-03-04 17:52:14 +08:00
jialin 8a98aef28c chore: image edit invert painting area 2025-03-04 17:52:14 +08:00
jialin bb47b4475e chore: per-commit lint 2025-03-03 14:30:55 +08:00
jialin 1bb6c5fbfc chore: init chat params 2025-03-03 10:13:06 +08:00
jialin 35fe459420 chore: init wrapper scroller 2025-03-03 10:13:06 +08:00
jialin d75ac45c33 fix: expand rows in models after operating 2025-02-28 20:07:03 +08:00
jialin 9927da5088 fix: do not init audio model 2025-02-28 12:23:02 +08:00
jialin 295b4b3b72 chore: download progress 2025-02-27 18:03:28 +08:00
jialin 8464e0ea3f feat: add download instance log button 2025-02-27 18:03:28 +08:00
jialin 4b73457f7f style: gpuselector dropdown style 2025-02-26 16:00:29 +08:00
jialin eedcf6f1fe style: select component 2025-02-26 16:00:29 +08:00
jialin 5b130d9500 fix: update first login state after deploying from catalog 2025-02-25 17:49:33 +08:00
jialin 600f457bc2 chore: update /v1-openai to /v1 in playground test code 2025-02-25 16:10:40 +08:00
jialin e1959f74aa chore: first login to default page by localhost 2025-02-25 15:04:05 +08:00
jialin 0f7d04e0b9 fix: page error when edit error 2025-02-25 14:03:14 +08:00
jialin f91311144b fix(style): layout content padding 2025-02-24 14:10:50 +08:00
jialin 529b3cb18f chore: get start from deploying 2025-02-24 11:47:06 +08:00
jialin bcbefe29e7 chore: get start from deploying 2025-02-24 11:34:40 +08:00
jialin 1dd7037aa0 fix: params form render depds on fields 2025-02-23 13:32:03 +08:00
jialin 2c2e9396c4 fix: default route for logo 2025-02-23 13:32:03 +08:00
jialin 69feb6a4f1 chore: image edit init params 2025-02-23 13:32:03 +08:00
jialin 911b4b9138 chore: audio init parametes 2025-02-23 13:32:03 +08:00
jialin 21cd4b67f0 chore: rerank init params 2025-02-23 13:32:03 +08:00
jialin 8c7eebe76c chore: embedding init params 2025-02-23 13:32:03 +08:00
jialin bb8b00e547 refactor: create image hooks 2025-02-23 13:32:03 +08:00
jialin f8c7f88ac2 feat: add custom modal for lock body scroller 2025-02-23 13:32:03 +08:00
jialin c6cc5810ca feat: add env field in deploy form 2025-02-21 17:05:06 +08:00
jialin a89ff584fd chore: minimize static files 2025-02-21 12:21:21 +08:00
jialin 11d824a88d fix: vox-box gpu-selector 2025-02-21 11:45:50 +08:00
jialin f1ab927a7e chore: chat hooks 2025-02-19 20:50:32 +08:00
jialin 9e0634f449 refactor: models list table columns from props 2025-02-19 15:17:17 +08:00
jialin a9006f2954 fix: do not need to open the row after batch updating 2025-02-18 20:06:20 +08:00
jialin 3a021e7879 fix: batch start/stop selected counts 2025-02-18 19:51:22 +08:00
jialin 6ea8cc9986 chore: copy model-tag from user-view 2025-02-18 19:51:22 +08:00
jialin 4c77151877 chore: render katex in markdown 2025-02-18 19:51:22 +08:00
jialin ea34431cf7 chore: react-markdown 2025-02-17 13:49:55 +08:00
jialin 7c546ed6b8 chore: update ui tips 2025-02-17 13:49:55 +08:00
jialin db53920153 style: menu style 2025-02-17 13:49:55 +08:00
jialin c60015bd09 chore: stt player ui 2025-02-17 13:49:55 +08:00
jialin d2a3388058 chore: improve image edit 2025-02-17 13:49:55 +08:00
jialin 384a75622f style: lock body scroll bar when open a modal 2025-02-17 13:49:55 +08:00
jialin 68a62d689c chore: batch start/stop models 2025-02-17 13:49:55 +08:00
jialin 8f7968302d style: model source dropdown 2025-02-17 13:49:55 +08:00
jialin 35a5733e82 chore: gpu selector, source dropdown 2025-02-17 13:49:55 +08:00
jialin f6bbbc56ba chore: negative mask on edit image 2025-02-17 13:49:55 +08:00
jialin d1d601f028 chore: tag style 2025-02-17 13:49:55 +08:00
jialin bbb133d0f2 refactor: gpu selector 2025-02-17 13:49:55 +08:00
jialin 9671f57b65 fix: import lodash in component 2025-02-12 19:34:09 +08:00
jialin d3212c8b49 refactor: message content 2025-02-12 19:34:09 +08:00
jialin 1f01a5b1cc chore: reasoning content without <think> tag 2025-02-11 17:41:55 +08:00
jialin 95f7f5535f fix: do not rerender after editing the markdown 2025-02-11 15:31:16 +08:00
jialin 8ae031dbbd style: markdown render 2025-02-11 14:01:46 +08:00
jialin 4498f069f0 feat: render markdown in chat 2025-02-11 14:01:46 +08:00
jialin 349b895ac5 feat: dcu container worker guide 2025-02-11 10:35:25 +08:00
jialin 794f27a3c8 chore: chat reasoning content 2025-02-10 21:53:36 +08:00
jialin 1def9a6b19 chore: field description icon 2025-02-10 18:44:21 +08:00
jialin cbf1e42c73 chore: local path field tooltip 2025-02-10 11:59:57 +08:00
jialin 7c960bb899 fix: set distributed_inference_across_workers to false when selectingt vox-box 2025-02-09 21:53:46 +08:00
jialin 1a1807df96 fix: download image size on edit page 2025-02-06 15:04:15 +08:00
jialin 2d03f17bde chore: refine wording 2025-02-06 11:07:38 +08:00
jialin 8cbaa6b281 fix: download image size is incorrect 2025-02-05 16:27:01 +08:00
jialin 2c0ae66143 style: side menu raw scroll bar width 0 2025-01-22 18:28:44 +08:00
jialin a54c160d83 fix: remove the constant deps for watch logs 2025-01-22 17:38:24 +08:00
jialin 61fb7b0d86 fix(style): tooltip scroll bar color, auto input trim spaces 2025-01-22 16:32:46 +08:00
jialin b3beae44df fix: child row expand status sync 2025-01-22 15:33:12 +08:00
jialin d1369e765a style: table no data 2025-01-21 14:25:14 +08:00
jialin 6185485c68 fix: update instances with full data 2025-01-21 10:31:40 +08:00
jialin db733ac9ce fix: json stream data is truncated 2025-01-20 22:08:08 +08:00
jialin f94fcf58a7 fix: update instances by filter 2025-01-20 17:47:36 +08:00
jialin 7487841385 chore: save iconfont files locally 2025-01-20 17:47:36 +08:00
jialin 289c4b497d chore: save iconfont files locally 2025-01-20 10:37:54 +08:00
jialin c49bfb97dd fix: clear instances when page hidden 2025-01-19 20:51:06 +08:00
jialin 4643dbce17 fix(style): brush can not fully paint the image 2025-01-19 00:00:15 +08:00
jialin 82e8c01b95 chore: set image raw size to form 2025-01-18 16:03:48 +08:00
jialin 47ced11e03 fix: reset update instance list flag 2025-01-17 23:39:10 +08:00
jialin 04d35a3a83 chore: all models page count 2025-01-17 23:22:49 +08:00
jialin 1df3a0b8cb fix: model instances update when tab active 2025-01-17 23:16:38 +08:00
jialin faa13f58e0 fix: catalog form meta data update 2025-01-17 21:22:15 +08:00
jialin 58fcc90417 fix: clear models expand keys 2025-01-17 14:35:36 +08:00
jialin 0690612477 fix: dashboard usage display 2025-01-16 22:27:04 +08:00
jialin c0344b385a fix: failed to load image when pass empty mask 2025-01-16 22:20:32 +08:00
jialin 214c5998c6 fix: image stream data error 2025-01-16 21:51:22 +08:00
jialin b14c01f9f7 fix: expand the model after deploying from catalog 2025-01-16 17:24:37 +08:00
jialin 4a838e7bdb chore: completions error handler 2025-01-16 14:25:03 +08:00
jialin 4a748e894d chore: rename copy button less 2025-01-16 10:33:18 +08:00
jialin b576491156 fix: remove storkes when use a new base image 2025-01-16 10:14:05 +08:00
jialin 17da790651 fix: update origin image uid for second edit 2025-01-15 22:58:27 +08:00
jialin c599cab14a fix: image display new image 2025-01-15 21:37:28 +08:00
jialin 4240bad627 fix: reranker selection 2025-01-15 19:36:44 +08:00
jialin 0017de7b21 fix: fixed point line width 2025-01-15 17:07:44 +08:00
jialin 53a4ebb66e fix: paste text can not overwrite the selection 2025-01-15 16:11:57 +08:00
jialin 5bfca0f86f chore: message link 2025-01-15 12:42:09 +08:00
jialin 9553aa944a fix: image progress display 2025-01-15 09:51:29 +08:00
jialin 35991ff5d6 chore: llama-box parameters 2025-01-14 18:58:07 +08:00
jialin 42d703ca1d chore: image edit scale 2025-01-14 17:11:46 +08:00
jialin e089ddb223 chore: image edit scale 2025-01-14 17:11:46 +08:00
jialin c323b6a8a9 chore: instance unreachable state 2025-01-14 09:40:15 +08:00
jialin bf461f59de fix: do not edit a image in loading 2025-01-13 19:25:30 +08:00
jialin 9974fa831c fix: image size options 2025-01-13 16:34:08 +08:00
jialin 73e41b0de0 fix: update page number of new logs 2025-01-13 15:59:32 +08:00
jialin 9aa297a804 chore: model file description 2025-01-13 15:51:34 +08:00
jialin 5c1bcdbd74 fix: clear screen logs after loading all 2025-01-13 11:15:34 +08:00
jialin 8c4997fa37 chore: image meta data 2025-01-13 08:22:23 +08:00
jialin d099a22bd9 fix: set last page after loading all logs 2025-01-12 20:01:27 +08:00
jialin 0de0426f72 fix: logs page by the data after parsing 2025-01-12 17:04:12 +08:00
jialin 04d5b692b8 fix: miss logs 2025-01-10 20:23:15 +08:00
jialin da6cdcef84 fix(style): gpu selector closable 2025-01-10 16:55:24 +08:00
jialin fbf2671eaf fix(style): gpu selector closable 2025-01-10 16:51:55 +08:00
jialin 67e050bd6f chore: unreachable state is not ready 2025-01-10 15:53:14 +08:00
jialin 8282f48cdb fix: calc log lines 2025-01-10 15:41:01 +08:00
jialin 5d903de2a2 chore: worker unreachable state 2025-01-10 12:25:06 +08:00
jialin 78deee2de3 chore: llm ctx max value 2025-01-10 10:49:06 +08:00
jialin a09403328e fix: do not update when scrolling to view logs 2025-01-10 10:42:41 +08:00
jialin 45e92e7b7a fix: image preview 2025-01-09 20:23:36 +08:00
jialin ebe8a40202 chore: update lock.yaml 2025-01-09 18:00:07 +08:00
jialin bd0747ff6f revert: image editor ux 2025-01-09 17:53:26 +08:00
jialin b37ec98e17 fix: set max_tokens to meta's half 2025-01-09 16:34:49 +08:00
jialin a93db0a038 fix(style): instance status message 2025-01-09 14:56:48 +08:00
jialin 08f9029bf8 chore: audio file .m4a support 2025-01-09 14:11:26 +08:00
jialin fb9d2d99d4 fix: display current page's logs 2025-01-09 11:27:44 +08:00
jialin a374d4d40f fix: body scrolls to top after opening a modal 2025-01-09 11:27:44 +08:00
jialin fb4666195c fix(style): catalog size tags overflow 2025-01-09 09:26:42 +08:00
jialin 39b6c00c6d fix: stop auto scrolling on logs 2025-01-08 15:43:03 +08:00
jialin decb3ef2f1 fix: add a button to last page on logs 2025-01-08 15:26:03 +08:00
jialin 2a9c501ee2 fix: form data merge with raw data 2025-01-08 13:45:05 +08:00
jialin 60d25a18fb chore: remove source in the form 2025-01-08 12:34:53 +08:00
jialin 39a71d51cf fix: image edit ux 2025-01-06 20:45:32 +08:00
jialin 5c401f3858 chore: catalog page size 100 2025-01-06 08:29:56 +08:00
jialin cc45ec85fd chore: models filter by category 2025-01-03 20:51:22 +08:00
jialin 1d66becff5 fix: default category 2025-01-03 20:51:22 +08:00
jialin 2a4ebcaf9c chore: search by debounce 2025-01-03 20:51:22 +08:00
jialin 1a792254ca chore: catalog card style 2025-01-03 20:51:22 +08:00
jialin d9319f5734 chore: catalog api 2025-01-03 20:51:22 +08:00
jialin 81d96fe3a9 style: catalog item style 2025-01-03 20:51:22 +08:00
jialin 39fff0e57d feat: catalog page 2025-01-03 20:51:22 +08:00
jialin 95058f7890 fix: do not update logs when viewing pre page 2025-01-03 20:38:44 +08:00
jialin 472ef1a231 chore: audio meta data 2025-01-03 17:27:19 +08:00
jialin d7de302393 fix: automatically update the page when scrolling 2025-01-02 12:22:09 +08:00
jialin e4b1dfc77a chore: audio page keepalive 2024-12-31 11:04:30 +08:00
jialin aed7267253 chore: model meta data 2024-12-30 16:07:30 +08:00
jialin a24b7433a5 chore: update edit api 2024-12-30 08:37:58 +08:00
jialin 0b423a6fe0 chore: image edit api 2024-12-30 08:37:58 +08:00
jialin cb98812444 chore: select multiple gpus by manual 2024-12-29 18:31:42 +08:00
jialin b4e30565d2 feat: model category 2024-12-27 19:12:51 +08:00
jialin 9f40e2c71e chore: watch request counts 2024-12-27 14:56:06 +08:00
jialin fb0a2798e7 chore: update img args 2024-12-27 14:56:06 +08:00
jialin 9912b5037d chore: disable editor when loading 2024-12-27 14:56:06 +08:00
jialin bca56bfaad chore: painting cache 2024-12-27 14:56:06 +08:00
jialin 542f048a67 chore: image edit ux 2024-12-27 14:56:06 +08:00
jialin 91c5e7e06c feat: image edit 2024-12-27 14:56:06 +08:00
jialin 972ee69fac style: view instance logs 2024-12-26 12:15:12 +08:00
jialin 00c9ef47e9 chore: search models tips 2024-12-25 16:48:20 +08:00
jialin dcad5f4b5f chore: success toast show twice after deleting 2024-12-25 11:36:02 +08:00
jialin e74cda3c0f chore: image args update 2024-12-24 16:43:19 +08:00
jialin bafbb39883 fix: backend input focus not work 2024-12-24 14:31:38 +08:00
jialin 02391e66dc chore: adjust the deletion modal config 2024-12-24 10:34:08 +08:00
jialin 762ab30c71 chore: add worker command paramters 2024-12-23 19:13:55 +08:00
jialin 6ce6a932e1 chore: add worker labels 2024-12-23 16:44:57 +08:00
jialin e7de1896ea chore: display the direct command in add worker 2024-12-23 16:33:08 +08:00
jialin 4391705c9a chore: model item action list order 2024-12-20 18:27:29 +08:00
jialin b40bf76270 chore: the text on the recreate button 2024-12-19 19:42:57 +08:00
jialin f44aee0af4 feat: model list add stop and start button 2024-12-18 16:13:58 +08:00
jialin 57a1af9392 fix: convert recording to mp3 2024-12-16 18:35:03 +08:00
jialin b16ae64105 fix: modelscope model tasks field null 2024-12-13 18:42:36 +08:00
jialin 0912d583c7 style: open in playground icon 2024-12-13 13:41:02 +08:00
jialin 6eff89aca1 style: audio tab icon 2024-12-13 13:17:07 +08:00
jialin 6e5e195a30 style: model list type tag 2024-12-13 12:13:17 +08:00
jialin e3efda2764 fix: deploy model page error 2024-12-13 10:15:46 +08:00
jialin 5360c3dbaf chore: mp3 converter exec load after init 2024-12-12 19:33:02 +08:00
jialin f6427c50f2 chore: update audio catalog for modelscope 2024-12-12 17:37:14 +08:00
jialin 22035d1383 chore: fetch model list after deleting action 2024-12-12 17:04:07 +08:00
jialin 85949c5927 fix: delay in updating model list 2024-12-12 16:28:14 +08:00
jialin dde746a5fe fix: do not collapse other models, after creating a new 2024-12-12 14:12:33 +08:00
jialin c2ae30575e fix: disable vox-box item when deploy ollama model 2024-12-12 11:49:46 +08:00
jialin cb5d9f626e fix: query is not updated on deploy page when tap enter 2024-12-12 10:03:16 +08:00
jialin c6d82409a8 chore: remove upload on image page 2024-12-11 18:44:00 +08:00
jialin fef5d0e688 fix: audio, recording format error 2024-12-11 18:39:36 +08:00
jialin f64e696f7a chore: support editing the model repoID, filename, model path 2024-12-10 15:43:29 +08:00
jialin acf99a6a9e refactor: generate code viewer 2024-12-09 15:28:18 +08:00
gitlawr a76560c38a chore: add LICENSE 2024-12-09 11:37:37 +08:00
jialin e1eec66e04 fix: text to speech python code 2024-12-05 20:54:28 +08:00
jialin 96d1ee2347 revert: speech to text code use openai lib 2024-12-05 19:50:29 +08:00
jialin 2a06463d07 fix: audio speech to text nodjs code 2024-12-05 18:20:13 +08:00
jialin 899862dc57 fix: audio view code 2024-12-05 17:54:20 +08:00
jialin c8d585889b fix: image random seed uncheck not work 2024-12-05 13:44:16 +08:00
jialin d044423330 fix: image handleonload callback 2024-12-04 21:03:42 +08:00
jialin 273a9a5e66 fix: convert recording to mp3 2024-12-04 20:37:09 +08:00
jialin 0294e0d2b3 fix: recording format for browser 2024-12-04 20:13:08 +08:00
jialin 38daa0826e fix: audio api error message 2024-12-04 19:02:10 +08:00
jialin b9e8dea5d9 chore: update audio docs link 2024-12-04 17:01:43 +08:00
jialin 241930cefe chore: rerank documents verify non-empty 2024-12-04 15:15:35 +08:00
jialin bed3684a0c fix: reset backend parameter options in deploy model modal 2024-12-04 11:15:29 +08:00
jialin e1bb684c4c chore: image size tooltips 2024-12-03 21:54:59 +08:00
jialin 4c75a5b421 fix: audio player pause 2024-12-03 20:21:20 +08:00
jialin 20f809740c fix: audio error message 2024-12-03 17:33:49 +08:00
jialin 83294abaf0 fix: audio set progress error 2024-12-03 16:17:54 +08:00
jialin 687dabc777 fix: audio set progress error 2024-12-03 16:17:54 +08:00
jialin 605d6f0f9b chore: update llama-box parameters 2024-12-03 16:17:54 +08:00
jialin b1d13f5a71 fix(style): image size cache' 2024-12-02 22:15:43 +08:00
jialin 50c740d219 fix(style): image wraper full size 2024-12-02 18:56:27 +08:00
jialin a7456d1182 fix: add cancel for generate text 2024-12-02 17:50:21 +08:00
jialin 804cbb8c56 style: image display ratio 2024-12-02 17:00:26 +08:00
jialin cb13f66b3d chore: voice language default value by current language 2024-12-02 10:44:39 +08:00
jialin ec2cd29096 chore: slider to change width and height in image 2024-12-02 08:20:30 +08:00
jialin 412c6dadca fix(style): stt loading 2024-11-29 22:10:52 +08:00
jialin 1ac3768468 fix: speech size 0, do not create url 2024-11-29 21:09:03 +08:00
jialin e622636686 style: show voice error message 2024-11-29 20:02:54 +08:00
jialin 2ddeca8f9d chore: add copy button 2024-11-29 17:03:46 +08:00
jialin d1a80e1a64 fix: tts show error message 2024-11-29 17:03:46 +08:00
jialin 4cfe41ca9e fix: image view code modal logs 2024-11-29 17:03:46 +08:00
jialin cd1d3005e9 fix: playground view code 2024-11-29 15:26:39 +08:00
jialin 435161854c fix: model logs loading 2024-11-29 13:41:28 +08:00
jialin 890287ce72 fix: image request error message 2024-11-29 10:58:16 +08:00
jialin 0cc2a40f26 chore: docker add worker command 2024-11-28 23:19:31 +08:00
jialin 849150c9ab fix: auto bgcolor remove 2024-11-28 22:03:33 +08:00
jialin 75ac74ca5e fix: image autosize 2024-11-28 22:03:33 +08:00
jialin 9822479d24 fix: remove image auto background 2024-11-28 21:58:28 +08:00
jialin c456f63e8c fix: jump to playground from model list 2024-11-28 21:58:28 +08:00
jialin 51ae3a16bd fix: jump to playground from model list 2024-11-28 21:48:14 +08:00
jialin bd418b23dc fix(style): playground image incomplete 2024-11-28 17:13:51 +08:00
jialin af789ee886 fix: rerank clear failed 2024-11-27 22:47:53 +08:00
jialin 324bd57e3d fix: image chunk data broken 2024-11-27 21:13:31 +08:00
jialin 146bcdcf1c fix: recording do not pass file type 2024-11-27 19:42:29 +08:00
jialin 60c92b8a98 chore: audio api adjust 2024-11-27 19:35:19 +08:00
jialin 230120fff5 chore: image custom params 2024-11-27 17:07:22 +08:00
jialin ba76498acc chore: audio model deploy 2024-11-27 11:04:31 +08:00
jialin 5e0772fae9 chore: images style 2024-11-25 20:07:17 +08:00
jialin 7fcd005f06 chore: audio 2024-11-25 20:07:17 +08:00
jialin 961d420c95 chore: encode url for hf 2024-11-25 12:58:27 +08:00
jialin a42df69a7f feat: add backend version field 2024-11-25 12:10:12 +08:00
jialin 59d2c5ae3c fix: safari page blank 2024-11-22 20:16:23 +08:00
jialin a6f0f554f0 fix: model be cleared when apply config to all in compare 2024-11-22 18:25:07 +08:00
jialin c8c22b3f22 fix: model markdown parse failed 2024-11-22 11:55:19 +08:00
jialin 67febd3e25 style: create images ux 2024-11-22 10:00:34 +08:00
jialin dde937f460 chore: images create 2024-11-22 10:00:34 +08:00
jialin c3473673da chore: speech to text 2024-11-22 10:00:34 +08:00
jialin f24aef300b feat: generate images 2024-11-22 10:00:34 +08:00
jialin 84e0c46711 feat: playground tts 2024-11-22 10:00:34 +08:00
jialin 16e2ac5b6c feat: playground embedding 2024-11-22 10:00:34 +08:00
jialin 01214077d3 fix: rebase main 2024-11-22 10:00:34 +08:00
jialin 1e3957bdd5 refactor: playground rerank layout 2024-11-22 10:00:34 +08:00
jialin 8e7fa486db style: submenu title color 2024-11-22 10:00:34 +08:00
jialin a72088b7a8 chore: rebase main 2024-11-22 10:00:34 +08:00
jialin 419f1e07d1 refactor: rerank item 2024-11-22 10:00:34 +08:00
jialin e05ec3d2d5 feat: playground add submenu 2024-11-22 10:00:34 +08:00
jialin 22f90a505c chore: hf api use proxy 2024-11-20 17:51:28 +08:00
jialin 66e506db07 fix: deploy model shardfile file name 2024-11-18 18:56:47 +08:00
jialin fa4041e560 style: status tooltip 2024-11-18 17:24:07 +08:00
jialin f8a8225b8c fix: close deploy modal reset form 2024-11-13 19:23:31 +08:00
jialin 75636b2575 fix: form fields do not update when toggle backend 2024-11-13 11:45:03 +08:00
jialin e44cc4c145 fix: update form options when open modal 2024-11-12 20:53:07 +08:00
jialin 343ada135d fix: backend updateable when local path 2024-11-08 15:09:25 +08:00
jialin 93a6a77c2b chore: deploy model from local path 2024-11-07 17:35:07 +08:00
jialin 8bc67b323c style: replace virtual inner with logs list 2024-11-06 09:29:12 +08:00
jialin f684a3b0ae fix: large logs page stuck 2024-11-05 19:46:47 +08:00
jialin 5f2115fff6 fix: page got stuck when logs size too large 2024-11-05 19:46:47 +08:00
jialin 9d654bcd66 fix: show upgrade check 2024-10-31 11:06:58 +08:00
jialin 5f8e72c6ae chore: update vllm arguments 2024-10-31 11:06:58 +08:00
jialin 5dde5cae50 fix: filter gguf files in modelscope 2024-10-29 17:55:40 +08:00
453 changed files with 43995 additions and 10688 deletions
+1
View File
@@ -1,2 +1,3 @@
PORT=9000
UMI_DEV_SERVER_COMPRESS=none
+1
View File
@@ -0,0 +1 @@
src/components/icon-font/iconfont/iconfont.js
+7 -1
View File
@@ -3,7 +3,13 @@ module.exports = {
rules: {
'react/no-unstable-nested-components': 1,
'no-unused-vars': 'off',
'no-undef': 'error',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/class-name-casing': 'off'
}
},
globals: {
Global: 'readonly',
React: 'readonly'
},
ignorePatterns: ['public/static/']
};
+18 -8
View File
@@ -4,11 +4,13 @@ on:
push:
branches:
- 'main'
- 'v*-dev'
tags:
- '*.*.*'
pull_request:
branches:
- 'main'
- 'v*-dev'
env:
NODE_VERSION: '21'
@@ -72,11 +74,6 @@ jobs:
if: github.event_name == 'push'
timeout-minutes: 30
runs-on: ubuntu-22.04
strategy:
matrix:
version: ['latest', '${{ github.ref_name }}']
exclude:
- version: 'main'
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -92,10 +89,23 @@ jobs:
${{ github.workspace }}/node_modules
- name: Build
run: scripts/build
- name: Determine Version
id: version
run: |
if [[ "${GITHUB_REF}" == "refs/heads/main" ]]; then
echo "version=latest" >> $GITHUB_OUTPUT
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
elif [[ "${GITHUB_REF}" == refs/heads/v*-dev ]]; then
echo "version=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT
else
echo "Not a publish target"
exit 1
fi
- name: Package
run: scripts/package
env:
VERSION: '${{ matrix.version }}'
VERSION: '${{ steps.version.outputs.version }}'
- name: Release
uses: TencentCloud/cos-action@b0aa648235fb35a1bdd6a77f529eb0ac4c2f1c25
with:
@@ -103,7 +113,7 @@ jobs:
secret_key: ${{ secrets.CI_TECENTCOS_SECRET_KEY }}
cos_bucket: ${{ secrets.COS_BUCKET }}
cos_region: ${{ secrets.COS_REGION }}
local_path: dist/${{ matrix.version }}.tar.gz
remote_path: releases/${{ matrix.version }}.tar.gz
local_path: dist/${{ steps.version.outputs.version }}.tar.gz
remote_path: releases/${{ steps.version.outputs.version }}.tar.gz
accelerate: true
clean: false
+1
View File
@@ -12,3 +12,4 @@
/.mfsu
.swc
.DS_Store
.idea
+4 -1
View File
@@ -1 +1,4 @@
npx --no-install lint-staged --quiet
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
+4 -8
View File
@@ -1,11 +1,6 @@
{
"*.{md,json}": [
"prettier --cache --write"
],
"*.{js,jsx}": [
"max lint --fix --eslint-only",
"prettier --cache --write"
],
"*.{md,json}": ["prettier --cache --write"],
"*.{js,jsx}": ["max lint --fix --eslint-only", "prettier --cache --write"],
"*.{css,less}": [
"max lint --fix --stylelint-only",
"prettier --cache --write"
@@ -13,5 +8,6 @@
"*.ts?(x)": [
"max lint --fix --eslint-only",
"prettier --cache --parser=typescript --write"
]
],
"src/locales/**/*.ts": ["npx tsx src/locales/check.ts"]
}
+6
View File
@@ -1,3 +1,9 @@
node_modules
.umi
.umi-production
public/static/*.js
public/static/*.css
src/components/icon-font/iconfont/iconfont.js
src/components/icon-font/iconfont/*.css
+2 -1
View File
@@ -2,5 +2,6 @@ module.exports = {
extends: require.resolve('@umijs/max/stylelint'),
rules: {
'selector-class-pattern': null
}
},
ignoreFiles: ['public/static/*.css']
};
+190
View File
@@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright (c) 2024 The GPUStack authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+9
View File
@@ -44,3 +44,12 @@ npm run dev
```
npm run build
```
## Bugs & Issues
- Please submit [bugs and issues](https://github.com/gpustack/gpustack/issues) with a label `ui`
## Links
- Official website: https://gpustack.ai/
- Documents: https://docs.gpustack.ai/latest/overview/
+18 -7
View File
@@ -1,7 +1,7 @@
import { defineConfig } from '@umijs/max';
import keepAlive from './keep-alive';
import proxy from './proxy';
import routes from './routes';
import theme from './theme';
import { getBranchInfo } from './utils';
const CompressionWebpackPlugin = require('compression-webpack-plugin');
@@ -14,11 +14,20 @@ const isProduction = env === 'production';
const t = Date.now();
export default defineConfig({
proxy: {
...proxy()
...proxy(process.env.PROXY_HOST)
},
history: {
type: 'hash'
},
analyze: {
analyzerMode: 'server',
analyzerPort: 8888,
openAnalyzer: true,
generateStatsFile: false,
statsFilename: 'stats.json',
logLevel: 'info',
defaultSizes: 'parsed' // stat // gzip
},
base: process.env.npm_config_base || '/',
...(isProduction
? {
@@ -41,6 +50,11 @@ export default defineConfig({
chunkFilename: `css/[name].${t}.chunk.css`
}
]);
config.module
.rule('worker')
.test(/\.worker\.js$/)
.use('worker-loader')
.loader('worker-loader');
config.output
.filename(`js/[name].${t}.js`)
.chunkFilename(`js/[name].${t}.chunk.js`);
@@ -65,17 +79,14 @@ export default defineConfig({
presets: ['umi-presets-pro'],
clickToComponent: {},
antd: {
style: 'less',
configProvider: {
componentSize: 'large',
theme
}
style: 'less'
},
hash: true,
access: {},
model: {},
initialState: {},
request: {},
keepalive: keepAlive,
locale: {
antd: true,
baseNavigator: true,
+6
View File
@@ -0,0 +1,6 @@
export const keepAliveRoutes = {
text2images: '/playground/text-to-image',
speech: '/playground/speech'
};
export default [keepAliveRoutes.text2images, keepAliveRoutes.speech];
+7
View File
@@ -18,7 +18,14 @@ export default function createProxyTable(target?: string) {
changeOrigin: true,
secure: false,
ws: true,
log: 'debug',
pathRewrite: (pth: string) => pth.replace(`/^/${api}`, `/${api}`),
// onProxyRes: (proxyRes: any, req: any, res: any) => {
// console.log('headers=========', {
// res: proxyRes.headers,
// req: req.headers
// });
// },
headers: {
origin: newTarget,
Connection: 'keep-alive'
+61 -7
View File
@@ -1,3 +1,5 @@
import { keepAliveRoutes } from './keep-alive';
export default [
{
name: 'dashboard',
@@ -9,19 +11,71 @@ export default [
},
{
name: 'playground',
title: 'Playground',
icon: 'ExperimentOutlined',
path: '/playground',
key: 'playground',
icon: 'Comment',
component: './playground'
routes: [
{
path: '/playground',
redirect: '/playground/chat'
},
{
name: 'chat',
title: 'Chat',
path: '/playground/chat',
key: 'chat',
icon: 'Comment',
component: './playground/index'
},
{
name: 'text2images',
title: 'Text2Images',
path: keepAliveRoutes.text2images,
key: 'text2images',
icon: 'Comment',
component: './playground/images'
},
{
name: 'speech',
title: 'Speech',
path: keepAliveRoutes.speech,
key: 'speech',
icon: 'Comment',
component: './playground/speech'
},
{
name: 'embedding',
title: 'embedding',
path: '/playground/embedding',
key: 'embedding',
icon: 'Comment',
component: './playground/embedding'
},
{
name: 'rerank',
title: 'Rerank',
path: '/playground/rerank',
key: 'rerank',
icon: 'Comment',
component: './playground/rerank'
}
]
},
{
name: 'modelCatalog',
path: '/models/catalog',
key: 'modelsCatalog',
icon: 'icon-catalog',
access: 'canSeeAdmin',
component: './llmodels/catalog'
},
{
name: 'models',
path: '/models',
path: '/models/list',
key: 'models',
icon: 'Block',
access: 'canSeeAdmin',
component: './llmodels'
component: './llmodels/index'
},
{
name: 'resources',
@@ -35,7 +89,7 @@ export default [
name: 'apikeys',
path: '/api-keys',
key: 'apikeys',
icon: 'LockOutlined',
icon: 'KeyOutlined',
component: './api-keys'
},
{
@@ -66,7 +120,7 @@ export default [
name: '404',
path: '*',
key: '404',
layout: false,
hideInMenu: true,
component: './404'
}
];
-55
View File
@@ -1,55 +0,0 @@
export default {
'root-entry-name': 'variable',
cssVar: true,
hashed: false,
components: {
Input: {
inputFontSize: 12
},
Table: {
cellFontSize: 12,
rowSelectedHoverBg: 'rgba(230, 230, 230, 0.88)',
rowSelectedBg: 'rgb(244, 245, 244)'
},
Menu: {
iconSize: 16,
iconMarginInlineEnd: 12,
itemHeight: 44,
itemColor: 'rgba(0,0,0,1)',
itemHoverBg: 'rgba(0,0,0,0.04)',
itemActiveBg: 'rgba(0,0,0,0.04)'
},
Progress: {
lineBorderRadius: 4
},
Select: {
itemSelectedBg: 'rgba(0,0,0,0.06)'
},
Message: {
contentPadding: '12px 16px'
},
Tooltip: {
colorBgSpotlight: 'rgba(110,110,110,1)'
// sizePopupArrow: 0
},
Slider: {
handleSize: 8,
handleSizeHover: 8,
trackBg: 'rgba(0,0,0,0.15)',
handleColor: 'rgba(0,0,0,0.2)',
trackHoverBg: 'rgba(0,0,0,0.25)',
handleActiveColor: 'rgba(0,0,0,0.3)',
dotActiveBorderColor: 'rgba(0,0,0,0.25)',
handleActiveOutlineColor: 'rgba(0,0,0,0.25)',
dotBorderColor: 'rgba(0,0,0,0.25)'
}
},
token: {
colorPrimary: '#007BFF',
colorSuccess: '#54cc98',
borderRadius: 4,
borderRadiusSM: 2,
fontSize: 12,
motion: true
}
};
+27
View File
@@ -1 +1,28 @@
// only for third party libraries styles
.simplebar-scrollbar.simplebar-visible::before {
opacity: 1;
}
.simplebar-scrollbar::before {
background: var(--scrollbar-handle-bg);
width: var(--scrollbar-size);
}
.simplebar-scrollbar.simplebar-visible.simplebar-hover {
&::before {
background: var(--scrollbar-handle-hover-bg);
}
}
.simplebar-scrollbar.scrollbar-handle-light {
&::before {
background-color: var(--scrollbar-handle-light-bg);
}
}
.simplebar-scrollbar.simplebar-visible.simplebar-hover.scrollbar-handle-light {
&::before {
background: var(--scrollbar-handle-light-bg);
}
}
+25 -3
View File
@@ -3,6 +3,7 @@
"author": "jialin",
"scripts": {
"build": "max build",
"check:locales": "npx tsx ./src/locales/check.ts",
"dev": "max dev",
"format": "prettier --cache --write .",
"postinstall": "max setup",
@@ -16,7 +17,8 @@
},
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@ant-design/pro-components": "^2.7.18",
"@ant-design/pro-components": "^2.7.19",
"@braintree/sanitize-url": "^7.1.1",
"@huggingface/gguf": "^0.1.7",
"@huggingface/hub": "^0.15.1",
"@huggingface/tasks": "^0.11.6",
@@ -27,32 +29,49 @@
"@umijs/max": "^4.2.11",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"ahooks": "^3.8.5",
"ansi-to-html": "^0.7.2",
"antd": "^5.20.6",
"antd": "^5.21.6",
"antd-style": "^3.6.2",
"axios": "^1.7.2",
"classnames": "^2.5.1",
"clipboard": "^2.0.11",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.11",
"driver.js": "^1.3.1",
"echarts": "^5.5.1",
"epubjs": "^0.3.93",
"has-ansi": "^5.0.1",
"highlight.js": "^11.10.0",
"jotai": "^2.8.4",
"jszip": "^3.10.1",
"katex": "^0.16.21",
"lamejs": "github:zhuker/lamejs",
"localforage": "^1.10.0",
"lodash": "^4.17.21",
"mammoth": "^1.8.0",
"marked": "^14.1.0",
"ml-dataset-iris": "^1.2.1",
"ml-pca": "^4.1.1",
"numeral": "^2.0.6",
"overlayscrollbars": "^2.10.0",
"overlayscrollbars-react": "^0.5.6",
"pdfjs-dist": "^4.7.76",
"query-string": "^9.0.0",
"rc-resize-observer": "^1.4.0",
"rc-virtual-list": "^3.14.8",
"re-resizable": "^6.10.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.5.0",
"react-markdown": "^9.0.3",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"simplebar-react": "^3.2.6",
"styled-components": "^6.1.15",
"umi-presets-pro": "^2.0.3",
"wavesurfer.js": "^7.8.8",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
},
"devDependencies": {
@@ -76,7 +95,10 @@
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-packagejson": "^2.5.0",
"prettier-plugin-two-style-order": "^1.0.1",
"tsx": "^4.19.3",
"typescript": "^5.4.5",
"url-loader": "^4.1.1"
"url-loader": "^4.1.1",
"webpack-bundle-analyzer": "^4.10.2",
"worker-loader": "^3.0.8"
}
}
+2982 -940
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+28 -3
View File
@@ -6,10 +6,28 @@ import {
queryVersionInfo,
updateCheck
} from '@/services/profile/apis';
import { isOnline } from '@/utils';
import {
IS_FIRST_LOGIN,
readState,
writeState
} from '@/utils/localstore/index';
import { RequestConfig, history } from '@umijs/max';
const loginPath = '/login';
// only for the first login and access from http://localhost
const checkDefaultPage = async (userInfo: any) => {
const isFirstLogin = await readState(IS_FIRST_LOGIN);
if (isFirstLogin === null && isOnline()) {
writeState(IS_FIRST_LOGIN, true);
if (userInfo && userInfo?.is_admin) {
history.push('/models/list');
}
}
};
// runtime configuration
export async function getInitialState(): Promise<{
fetchUserInfo: () => Promise<Global.UserInfo>;
@@ -20,7 +38,10 @@ export async function getInitialState(): Promise<{
const getUpdateCheck = async () => {
try {
const data = await updateCheck();
setAtomStorage(UpdateCheckAtom, data);
setAtomStorage(UpdateCheckAtom, {
...data
});
return data;
} catch (error) {
console.error('updateCheck error', error);
@@ -45,7 +66,11 @@ export async function getInitialState(): Promise<{
const getAppVersionInfo = async () => {
try {
const data = await queryVersionInfo();
setAtomStorage(GPUStackVersionAtom, data);
const isProduction = data.version?.indexOf('0.0.0') === -1;
setAtomStorage(GPUStackVersionAtom, {
...data,
isProduction
});
} catch (error) {
console.error('queryVersionInfo error', error);
}
@@ -55,7 +80,7 @@ export async function getInitialState(): Promise<{
if (![loginPath].includes(location.pathname)) {
const userInfo = await fetchUserInfo();
checkDefaultPage(userInfo);
return {
fetchUserInfo,
currentUser: userInfo
Binary file not shown.

After

Width:  |  Height:  |  Size: 726 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

+140
View File
@@ -6,6 +6,10 @@
margin-bottom: 8px;
}
.m-b-6 {
margin-bottom: 6px;
}
.m-b-5 {
margin-bottom: 5px;
}
@@ -42,6 +46,18 @@
margin-left: 40px;
}
.m-t-16 {
margin-top: 16px;
}
.m-t-10 {
margin-top: 10px;
}
.m-0 {
margin: 0;
}
.m-r-10 {
margin-right: 10px;
}
@@ -74,9 +90,14 @@
padding-left: 8px;
}
.m-t-6 {
margin-top: 6px;
}
.flex {
display: flex;
}
// space-between
.flex-between {
display: flex;
@@ -87,6 +108,17 @@
display: flex;
justify-content: center;
}
.justify-start {
display: flex;
justify-content: flex-start;
}
.justify-end {
display: flex;
justify-content: flex-end;
}
// align-items: center
.flex-center {
display: flex;
@@ -132,6 +164,18 @@
gap: 8px;
}
.gap-20 {
gap: 20px;
}
.gap-6 {
gap: 6px;
}
.line-24 {
line-height: 24px;
}
.relative {
position: relative;
}
@@ -144,6 +188,10 @@
font-size: var(--font-size-base);
}
.font-size-13 {
font-size: 13px;
}
.font-size-14 {
font-size: var(--font-size-middle);
}
@@ -156,6 +204,26 @@
font-size: 20px;
}
.font-size-24 {
font-size: 24px;
}
.font-size-32 {
font-size: 32px;
}
.m-b-0 {
margin-bottom: 0;
}
.m-b-16 {
margin-bottom: 16px;
}
.m-r-16 {
margin-right: 16px;
}
.font-400 {
font-weight: var(--font-weight-normal);
}
@@ -164,6 +232,10 @@
font-weight: var(--font-weight-medium);
}
.font-500 {
font-weight: var(--font-weight-500);
}
.font-700 {
font-weight: var(--font-weight-bold);
}
@@ -175,3 +247,71 @@
.text-tertiary {
color: var(--ant-color-text-tertiary);
}
.tips-desc-list {
list-style-type: decimal;
padding-left: 16px;
}
.gap-16 {
gap: 16px;
}
.color-white-tertiary {
color: var(--color-white-tertiary);
}
.color-white-quaternary {
color: var(--color-white-quaternary);
}
.color-white-light-1 {
color: var(--color-white-light-1);
}
.color-white-light-2 {
color: var(--color-white-light-2);
}
.color-white-light-3 {
color: var(--color-white-light-3);
}
.color-white-light-4 {
color: var(--color-white-light-4);
}
textarea {
padding-inline: 6px 0;
padding-block: 0;
background-color: transparent;
}
textarea::-webkit-scrollbar {
width: var(--scrollbar-size);
}
textarea::-webkit-scrollbar-track {
background-color: transparent;
}
textarea::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 6px;
}
textarea:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 6px;
}
}
.bold-text {
color: var(--ant-color-text);
font-weight: var(--font-weight-medium);
}
.ant-result-extra {
color: var(--ant-color-text-secondary);
}
+9
View File
@@ -0,0 +1,9 @@
.driver-popover {
color: var(--ant-color-text);
.driver-popover-title {
font-size: 16px;
font-weight: var(--font-weight-bold);
color: var(--ant-color-text);
}
}
-4
View File
@@ -1,10 +1,6 @@
.ant-pro-layout {
.ant-pro-sider {
// &.ant-layout-sider {
// background: var(--color-white-1);
// }
.ant-layout-sider-children {
background-color: var(--color-fill-1);
border-inline: none;
border-radius: 0;
// padding-inline: 16px;
+4
View File
@@ -0,0 +1,4 @@
import { atom } from 'jotai';
// models expand keys: create, update , delete,
export const modelsExpandKeysAtom = atom<string[]>([]);
+17
View File
@@ -0,0 +1,17 @@
import { atom, getDefaultStore } from 'jotai';
export const routeCacheAtom = atom<any>(new Map());
export const setRouteCache = (key: string, value: any) => {
const store = getDefaultStore();
const cache = store.get(routeCacheAtom);
cache.set(key, value);
store.set(routeCacheAtom, cache);
};
export const deleteRouteCache = (key: string) => {
const store = getDefaultStore();
const cache = store.get(routeCacheAtom);
cache.delete(key);
store.set(routeCacheAtom, cache);
};
+53
View File
@@ -0,0 +1,53 @@
import { colorPrimary } from '@/config/theme';
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
type UserSettings = {
theme: 'light' | 'realDark';
mode: 'light' | 'realDark' | 'auto';
colorPrimary: string;
isDarkTheme: boolean;
};
const defaultSettings: UserSettings = {
theme: 'light',
mode: 'auto',
isDarkTheme: false,
colorPrimary: colorPrimary
};
export const getStorageUserSettings = () => {
if (typeof window === 'undefined') return defaultSettings;
try {
const savedSettings = JSON.parse(
localStorage.getItem('userSettings') || '{}'
);
return {
...defaultSettings,
...savedSettings
};
} catch {
return defaultSettings;
}
};
export const userSettingsAtom = atomWithStorage<UserSettings>(
'userSettings',
defaultSettings
);
export const userSettingsHelperAtom = atom(
(get) => get(userSettingsAtom),
(get, set, update: Partial<UserSettings>) => {
const prev = get(userSettingsAtom);
const newSettings = {
...prev,
...(update || {})
};
set(userSettingsAtom, {
...newSettings,
colorPrimary: newSettings.colorPrimary || colorPrimary,
isDarkTheme: newSettings.theme === 'realDark'
});
}
);
+20
View File
@@ -0,0 +1,20 @@
import { getDefaultStore } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
export const tabActiveAtom = atomWithStorage<Map<string, any>>(
'tabActiveStatus',
new Map()
);
export const setActiveStatus = (key: string, value: any) => {
const store = getDefaultStore();
const cache = store.get(tabActiveAtom);
cache.set(key, value);
store.set(tabActiveAtom, cache);
};
export const getActiveStatus = (key: string) => {
const store = getDefaultStore();
const cache = store.get(tabActiveAtom);
return cache.get(key);
};
+3 -1
View File
@@ -6,9 +6,11 @@ export const userAtom = atomWithStorage<any>('userInfo', null);
export const GPUStackVersionAtom = atom<{
version: string;
git_commit: string;
isProduction: boolean;
}>({
version: '',
git_commit: ''
git_commit: '',
isProduction: false
});
export const UpdateCheckAtom = atom<{
+45
View File
@@ -0,0 +1,45 @@
interface WatchRequest {
id: number;
token: any;
cancel: () => void;
}
declare global {
interface Window {
__GPUSTACK_WATCH_REQUEST_CLEAR__: {
watchIDValue: number;
requestList: WatchRequest[];
};
}
}
window.__GPUSTACK_WATCH_REQUEST_CLEAR__ = {
watchIDValue: 0,
requestList: []
};
export const updateWatchIDValue = () => {
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.watchIDValue =
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.watchIDValue + 1;
return window.__GPUSTACK_WATCH_REQUEST_CLEAR__.watchIDValue;
};
export const updateWatchRequest = (watchToken: WatchRequest) => {
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList.push(watchToken);
};
export const cancelWatchRequest = (n: number) => {
// cancel the before n requests
const requestList = window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList;
for (let i = 0; i < n; i++) {
requestList[i]?.cancel?.();
}
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList = requestList.slice(n);
};
export const clearWatchRequestId = (id: number) => {
const requestList = window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList;
const newRequestList = requestList.filter((item) => item.id !== id);
window.__GPUSTACK_WATCH_REQUEST_CLEAR__.requestList = newRequestList;
};
+72
View File
@@ -0,0 +1,72 @@
.alert-info-block {
padding-block: 6px;
padding-inline: 10px 16px;
position: relative;
padding-left: 32px;
text-align: left;
border-radius: var(--border-radius-base);
margin: 0;
border: 1px solid transparent;
.ant-typography {
margin-bottom: 0;
}
&.danger {
border-color: var(--ant-color-error-border);
background-color: var(--ant-color-error-bg);
}
&.warning {
border-color: var(--ant-color-warning-border);
background-color: var(--ant-color-warning-bg);
}
&.transition {
color: var(--ant-geekblue-7);
background: var(--ant-geekblue-1);
border-color: var(--ant-geekblue-3);
}
&.success {
border: 1px solid var(--ant-color-success);
color: var(--ant-color-success);
background: var(--ant-color-success-bg);
.content.success {
font-weight: var(--font-weight-normal);
}
}
.title {
position: absolute;
left: 0;
top: 0;
display: flex;
height: 32px;
padding: 5px 10px;
border-radius: var(--border-radius-base) var(--border-radius-base) 0 0;
.info-icon {
&.danger {
color: var(--ant-color-error);
}
&.warning {
color: var(--ant-color-warning);
}
&.transition {
color: var(--ant-geekblue-7);
}
&.success {
color: var(--ant-color-success);
}
}
.text {
font-weight: var(--font-weight-bold);
}
}
}
+85
View File
@@ -0,0 +1,85 @@
import { WarningFilled } from '@ant-design/icons';
import { Typography } from 'antd';
import classNames from 'classnames';
import React from 'react';
import styled from 'styled-components';
import OverlayScroller from '../overlay-scroller';
import './block.less';
interface AlertInfoProps {
type: Global.MessageType;
message: React.ReactNode;
rows?: number;
icon?: React.ReactNode;
ellipsis?: boolean;
style?: React.CSSProperties;
contentStyle?: React.CSSProperties;
title?: React.ReactNode;
maxHeight?: number;
}
const TitleWrapper = styled.div`
font-weight: 700;
color: var(--ant-color-text);
`;
const ContentWrapper = styled.div<{ $hasTitle: boolean }>`
word-break: break-word;
color: ${(props) =>
props.$hasTitle
? 'var(--ant-color-text-secondary)'
: 'var(--ant-color-text)'};
font-weight: var(--font-weight-500);
`;
const AlertInfo: React.FC<AlertInfoProps> = (props) => {
const {
message,
type,
rows = 1,
ellipsis,
style,
title,
contentStyle,
icon,
maxHeight = 86
} = props;
return (
<>
{message ? (
<div
className={classNames('alert-info-block', type)}
style={{ ...style }}
>
<Typography.Paragraph
ellipsis={
ellipsis ?? {
rows: rows,
tooltip: message
}
}
>
<div className={classNames('title', type)}>
<span className={classNames('info-icon', type)}>
{icon ?? <WarningFilled />}
</span>
</div>
{title && (
<TitleWrapper className="title-text">{title}</TitleWrapper>
)}
<OverlayScroller maxHeight={maxHeight} style={{ ...contentStyle }}>
<ContentWrapper
$hasTitle={!!title}
className={classNames('content', type)}
>
{message}
</ContentWrapper>
</OverlayScroller>
</Typography.Paragraph>
</div>
) : null}
</>
);
};
export default React.memo(AlertInfo);
+47
View File
@@ -0,0 +1,47 @@
import { WarningOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import React from 'react';
interface AlertInfoProps {
type: 'danger' | 'warning';
message: string;
rows?: number;
icon?: React.ReactNode;
ellipsis?: boolean;
style?: React.CSSProperties;
}
const AlertInfo: React.FC<AlertInfoProps> = (props) => {
const { message, type, rows = 1, ellipsis, style } = props;
return (
<>
{message ? (
<Typography.Paragraph
type={type}
ellipsis={
ellipsis !== undefined
? ellipsis
: {
rows: rows,
tooltip: message
}
}
style={{
textAlign: 'center',
padding: '2px 5px',
borderRadius: 'var(--border-radius-base)',
margin: 0,
backgroundColor: 'var(--ant-color-error-bg)',
...style
}}
>
<WarningOutlined className="m-r-8" />
{message}
</Typography.Paragraph>
) : null}
</>
);
};
export default React.memo(AlertInfo);
+19
View File
@@ -0,0 +1,19 @@
.canvas-wrap {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
width: 100%;
canvas {
display: block;
width: 100%;
image-rendering: crisp-edges;
}
}
.scroller-wrapper {
width: 100%;
height: 100%;
overflow: hidden;
}
+192
View File
@@ -0,0 +1,192 @@
import useResizeObserver from '@/components/logs-viewer/use-size';
import React, { useEffect, useState } from 'react';
import './index.less';
interface AudioAnimationProps {
width: number;
height: number;
maxWidth?: number;
scaleFactor?: number;
maxBarCount?: number;
amplitude?: number;
fixedHeight?: boolean;
analyserData: {
data: Uint8Array;
analyser: any;
};
}
const AudioAnimation: React.FC<AudioAnimationProps> = (props) => {
const {
scaleFactor = 1.2,
maxBarCount = 128,
amplitude = 40,
maxWidth,
fixedHeight = true,
analyserData,
width: initialWidth,
height: initialHeight
} = props;
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const animationId = React.useRef<number>(0);
const isScaled = React.useRef<boolean>(false);
const oscillationOffset = React.useRef(0);
const direction = React.useRef(1);
const scrollerRef = React.useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(initialWidth);
const [height, setHeight] = useState(initialHeight);
const containerRef = React.useRef<any>(null);
const size = useResizeObserver(scrollerRef);
const calculateJitter = (
i: number,
timestamp: number,
baseHeight: number,
minJitter: number,
jitterAmplitude: number
) => {
//
const jitterFactor = Math.sin(timestamp / 200 + i) * 0.5 + 0.5;
const jitter =
minJitter +
jitterFactor * (jitterAmplitude - minJitter) * (baseHeight / maxBarCount);
return jitter;
};
const startAudioVisualization = () => {
if (!canvasRef.current || !analyserData.data?.length) return;
const canvas = canvasRef.current;
const canvasCtx = canvas.getContext('2d');
if (!canvasCtx) return;
const WIDTH = (canvas.width = width * 2);
const HEIGHT = (canvas.height = height * 2);
if (!isScaled.current) {
canvasCtx.scale(2, 2);
isScaled.current = true;
}
const barWidth = 4;
const barSpacing = 6;
const centerLine = Math.floor(HEIGHT / 2);
const jitterAmplitude = amplitude;
const minJitter = 10;
let lastFrameTime = 0;
const gradient = canvasCtx.createLinearGradient(0, 0, 0, HEIGHT);
gradient.addColorStop(0, '#007BFF');
gradient.addColorStop(1, '#0069DA');
canvasCtx.fillStyle = gradient;
const draw = (timestamp: number) => {
const elapsed = timestamp - lastFrameTime;
if (elapsed < 16) {
animationId.current = requestAnimationFrame(draw);
return;
}
lastFrameTime = timestamp;
analyserData.analyser?.current?.getByteFrequencyData(analyserData.data);
canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
const barCount = Math.min(maxBarCount, analyserData.data.length);
const totalWidth = barCount * (barWidth + barSpacing) - barSpacing;
let x = WIDTH / 2 - totalWidth / 2 + oscillationOffset.current;
oscillationOffset.current += direction.current;
if (Math.abs(oscillationOffset.current) > 20) {
direction.current *= -1;
}
for (let i = 0; i < barCount; i++) {
const baseHeight = Math.floor(analyserData.data[i] / 2) * scaleFactor;
const jitter = calculateJitter(
i,
timestamp,
baseHeight,
minJitter,
jitterAmplitude
);
const barHeight = baseHeight + Math.round(jitter);
const topY = Math.round(centerLine - barHeight / 2);
const bottomY = Math.round(centerLine + barHeight / 2);
canvasCtx.beginPath();
canvasCtx.moveTo(x, bottomY);
canvasCtx.lineTo(x, topY + 2);
canvasCtx.arcTo(x + barWidth, topY + 2, x + barWidth, bottomY, 2);
canvasCtx.lineTo(x + barWidth, bottomY);
canvasCtx.closePath();
canvasCtx.fill();
x += barWidth + barSpacing;
}
animationId.current = requestAnimationFrame(draw);
};
draw(performance.now());
};
React.useEffect(() => {
if (size) {
if (maxWidth) {
setWidth(Math.min(size.width, maxWidth));
} else {
setWidth(size?.width || 0);
}
if (!fixedHeight) {
setHeight(size?.height || 0);
}
}
}, [size, maxWidth]);
useEffect(() => {
if (!canvasRef.current) return;
const clearCanvas = () => {
if (canvasRef.current) {
const ctx = canvasRef.current.getContext('2d');
if (ctx) ctx.clearRect(0, 0, width * 2, height * 2);
}
};
if (!analyserData.data?.length || !analyserData.analyser?.current) {
clearCanvas();
cancelAnimationFrame(animationId.current);
animationId.current = 0;
return;
}
startAudioVisualization();
return () => {
cancelAnimationFrame(animationId.current);
clearCanvas();
};
}, [analyserData, width, height]);
return (
<div
className="scroller-wrapper"
ref={scrollerRef}
style={{ width: '100%', height: '100%' }}
>
<div
ref={containerRef}
className="canvas-wrap"
style={{ width: '100%', height: '100%' }}
>
<canvas ref={canvasRef} style={{ display: 'block' }}></canvas>
</div>
</div>
);
};
export default React.memo(AudioAnimation);
@@ -0,0 +1,39 @@
export type AudioEvent =
| 'play'
| 'playing'
| 'pause'
| 'timeupdate'
| 'ended'
| 'loadedmetadata'
| 'audioprocess'
| 'canplay'
| 'ended'
| 'loadeddata'
| 'seeked'
| 'seeking'
| 'volumechange';
export interface AudioPlayerProps {
controls?: boolean;
autoplay?: boolean;
url: string;
speed?: number;
ref?: any;
height?: number;
width?: number;
duration?: number;
onPlay?: () => void;
onPlaying?: () => void;
onPause?: () => void;
onTimeUpdate?: () => void;
onEnded?: () => void;
onLoadedMetadata?: (duration: number) => void;
onAudioProcess?: (current: number) => void;
onCanPlay?: () => void;
onLoadedData?: () => void;
onSeeked?: () => void;
onSeeking?: () => void;
onVolumeChange?: () => void;
onReady?: (duration: number) => void;
onAnalyse?: (analyseData: any, frequencyBinCount: any) => void;
}
+104
View File
@@ -0,0 +1,104 @@
.player-wrap {
width: 100%;
display: flex;
// background-color: var(--ant-color-fill-quaternary);
border-radius: 6px;
.player-ui {
padding: 8px 16px;
flex: 1;
display: flex;
justify-content: flex-start;
align-items: center;
}
.controls {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.play-btn {
margin-inline: 30px;
height: 22px;
width: 22px;
overflow: hidden;
.ant-btn {
height: 22px;
width: 22px;
}
}
.backward,
.forward {
background: none !important;
}
.slider {
display: flex;
justify-content: center;
align-items: center;
.slider-inner {
flex: 1;
}
}
.play-content {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
}
.time {
width: 52px;
text-align: right;
&.current {
text-align: left;
}
}
.progress-bar {
margin-inline: 10px;
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
.slider {
width: 100%;
}
.file-name {
margin-bottom: 6px;
line-height: 20px;
height: 20px;
display: flex;
align-items: center;
align-self: center;
justify-content: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.ant-slider-horizontal {
margin-block: 5px;
}
}
.speaker {
margin-left: 10px;
position: relative;
.volume-slider {
position: absolute;
bottom: 30px;
}
}
}
+311
View File
@@ -0,0 +1,311 @@
import { formatTime } from '@/utils/index';
import {
FastBackwardOutlined,
FastForwardOutlined,
PauseCircleFilled,
PlayCircleFilled
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Slider, Tooltip } from 'antd';
import { round } from 'lodash';
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle
} from 'react';
import './index.less';
interface AudioPlayerProps {
autoplay?: boolean;
url: string;
speed?: number;
ref?: any;
name: string;
height?: number;
width?: number;
duration?: number;
extra?: React.ReactNode;
}
const speedOptions = [
{ label: '1x', value: 1 },
{ label: '2x', value: 2 },
{ label: '3x', value: 3 },
{ label: '4x', value: 4 }
];
const speedConfig = {
min: 0.5,
max: 2,
step: 0.25
};
const AudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
const intl = useIntl();
const { autoplay = false, speed: defaultSpeed = 1, extra } = props;
const audioRef = React.useRef<HTMLAudioElement>(null);
const [audioState, setAudioState] = React.useState<{
currentTime: number;
duration: number;
}>({
currentTime: 0,
duration: 0
});
const [playOn, setPlayOn] = React.useState<boolean>(false);
const [speakerOn, setSpeakerOn] = React.useState<boolean>(false);
const [volume, setVolume] = React.useState<number>(1);
const [speed, setSpeed] = React.useState<number>(defaultSpeed);
const timer = React.useRef<any>(null);
useImperativeHandle(ref, () => ({
play: () => {
audioRef.current?.play();
},
pause: () => {
audioRef.current?.pause();
}
}));
const handleShowVolume = useCallback(() => {
setSpeakerOn(!speakerOn);
}, [speakerOn]);
const handleSeepdChange = useCallback((value: number | string) => {
setSpeed(value as number);
audioRef.current!.playbackRate = value as number;
}, []);
const handleAudioOnPlay = useCallback(() => {
console.log('audio play');
timer.current = setInterval(() => {
setAudioState((prestate) => {
return {
currentTime: Math.ceil(audioRef.current?.currentTime || 0),
duration:
prestate.duration || Math.ceil(audioRef.current?.duration || 0)
};
});
if (audioRef.current?.paused || audioRef.current?.ended) {
clearInterval(timer.current);
setPlayOn(false);
setAudioState((prestate: any) => {
return {
currentTime: audioRef.current?.ended ? 0 : prestate.currentTime,
duration: prestate.duration
};
});
}
}, 500);
}, []);
const handlePlay = useCallback(() => {
setPlayOn(!playOn);
if (playOn) {
audioRef.current?.pause();
} else {
audioRef.current?.play();
}
}, [playOn]);
const handleFormatVolume = (val?: number) => {
if (val === undefined) {
return `${round(volume * 100)}%`;
}
return `${round(val * 100)}%`;
};
const handleVolumeChange = useCallback((value: number) => {
audioRef.current!.volume = round(value, 2);
setVolume(round(value, 2));
}, []);
const initPlayerConfig = () => {
if (audioRef.current) {
audioRef.current!.volume = volume;
audioRef.current!.playbackRate = speed;
}
};
const handleLoadedMetadata = useCallback(
(data: any) => {
const duration = Math.ceil(audioRef.current?.duration || 0);
setAudioState({
currentTime: 0,
duration:
duration && duration !== Infinity ? duration : props.duration || 0
});
setPlayOn(autoplay);
},
[autoplay, props.duration]
);
const handleCurrentChange = useCallback((val: number) => {
audioRef.current!.currentTime = val;
setAudioState((prestate) => {
return {
currentTime: val,
duration: prestate.duration
};
});
}, []);
const handleReduceSpeed = () => {
setSpeed((pre) => {
if (pre - speedConfig.step < speedConfig.min) {
return speedConfig.min;
}
const next = pre - speedConfig.step;
audioRef.current!.playbackRate = next;
return next;
});
};
const handleAddSpeed = () => {
setSpeed((pre) => {
if (pre + speedConfig.step > speedConfig.max) {
return speedConfig.max;
}
const next = pre + speedConfig.step;
audioRef.current!.playbackRate = next;
return next;
});
};
const handleOnLoad = (e: any) => {
console.log('onload', e);
};
const onDownload = useCallback(() => {
const url = props.url || '';
const filename = props.name;
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
}, [props.url, props.name]);
useEffect(() => {
if (audioRef.current) {
initPlayerConfig();
}
}, [audioRef.current]);
useEffect(() => {
return () => {
clearInterval(timer.current);
};
}, []);
return (
<div className="player-wrap" style={{ width: props.width || '100%' }}>
<div className="player-ui">
<div className="play-content">
<div className="progress-bar">
<span className="file-name">{props.name}</span>
<div className="slider">
{/* <span className="time current">
{' '}
{formatTime(audioState.currentTime)}
</span> */}
<div className="slider-inner">
<Slider
tooltip={{ open: false }}
min={0}
step={1}
styles={{
rail: {
// height: 6
}
}}
max={audioState.duration}
value={audioState.currentTime}
onChange={handleCurrentChange}
/>
</div>
{/* <span className="time">{formatTime(audioState.duration)}</span> */}
</div>
<div className="controls">
<div className="audio-control flex-center">
<span className="time current">
{' '}
{formatTime(audioState.currentTime)}
</span>
<Tooltip
title={intl.formatMessage({
id: 'playground.audio.button.slow'
})}
>
<Button
type="text"
size="small"
className="backward"
disabled={
speed === speedConfig.min || speed < speedConfig.min
}
onClick={handleReduceSpeed}
>
<FastBackwardOutlined className="font-size-20" />
</Button>
</Tooltip>
<span className="play-btn">
<Button
size="middle"
type="text"
onClick={handlePlay}
disabled={!audioState?.duration}
icon={
!playOn ? (
<PlayCircleFilled
style={{ fontSize: '22px' }}
></PlayCircleFilled>
) : (
<PauseCircleFilled
style={{ fontSize: '22px' }}
></PauseCircleFilled>
)
}
></Button>
</span>
<Tooltip
title={intl.formatMessage({
id: 'playground.audio.button.fast'
})}
>
<Button
type="text"
size="small"
className="forward"
disabled={
speed === speedConfig.max || speed > speedConfig.max
}
onClick={handleAddSpeed}
>
<FastForwardOutlined className="font-size-20" />
</Button>
</Tooltip>
<span className="time">{formatTime(audioState.duration)}</span>
</div>
{extra}
</div>
</div>
</div>
</div>
<audio
crossOrigin="anonymous"
autoPlay={autoplay}
src={props.url}
ref={audioRef}
preload="metadata"
style={{ opacity: 0, position: 'absolute', left: '-9999px' }}
onPlay={handleAudioOnPlay}
onLoadedMetadata={handleLoadedMetadata}
></audio>
</div>
);
});
export default React.memo(AudioPlayer);
@@ -0,0 +1,165 @@
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef
} from 'react';
import { AudioPlayerProps } from './config/type';
const RawAudioPlayer: React.FC<AudioPlayerProps> = forwardRef((props, ref) => {
const { autoplay = false } = props;
const audioRef = React.useRef<HTMLAudioElement>(null);
// =================== audio context ======================
const audioContext = useRef<any>(null);
const analyser = useRef<any>(null);
const dataArray = useRef<any>(null);
// ========================================================
const initAudioContext = useCallback(() => {
audioContext.current = new (window.AudioContext ||
window.webkitAudioContext)();
analyser.current = audioContext.current.createAnalyser();
analyser.current.fftSize = 512;
dataArray.current = new Uint8Array(analyser.current.frequencyBinCount);
}, []);
const generateVisualData = useCallback(() => {
const source = audioContext.current.createMediaElementSource(
audioRef.current
);
source.connect(analyser.current);
analyser.current.connect(audioContext.current.destination);
}, []);
const initEnvents = () => {
if (!audioRef.current) {
return;
}
audioRef.current.addEventListener('complete', () => {});
audioRef.current.addEventListener('play', () => {
props.onAnalyse?.(dataArray.current, analyser);
props.onPlay?.();
});
audioRef.current.addEventListener('pause', () => {
props.onAnalyse?.(dataArray.current, analyser);
props.onPause?.();
});
audioRef.current.addEventListener('timeupdate', () => {
props.onTimeUpdate?.();
});
audioRef.current.addEventListener('ended', () => {
props.onEnded?.();
});
// add all other events
audioRef.current.addEventListener('canplay', () => {
props.onCanPlay?.();
});
audioRef.current.addEventListener('loadeddata', () => {
initEnvents();
if (!audioContext.current) {
initAudioContext();
generateVisualData();
}
props.onLoadedData?.();
});
audioRef.current.addEventListener('seeked', () => {
props.onSeeked?.();
});
audioRef.current.addEventListener('seeking', () => {
props.onSeeking?.();
});
audioRef.current.addEventListener('volumechange', () => {
props.onVolumeChange?.();
});
audioRef.current.addEventListener('audioprocess', () => {
const current = audioRef.current?.currentTime || 0;
props.onAudioProcess?.(current);
});
audioRef.current.addEventListener('playing', () => {
props.onPlaying?.();
});
audioRef.current.addEventListener('loadedmetadata', () => {
const duration = audioRef.current?.duration || 0;
props.onLoadedMetadata?.(duration);
props.onReady?.(duration);
});
audioRef.current.addEventListener('ended', () => {
props.onEnded?.();
});
audioRef.current.addEventListener('loadeddata', () => {
props.onLoadedData?.();
});
};
useImperativeHandle(ref, () => ({
play: () => {
audioRef.current?.play();
},
pause: () => {
audioRef.current?.pause();
}
}));
useEffect(() => {
if (audioRef.current) {
console.log('audioRef.current', audioRef.current, props.url);
initEnvents();
}
return () => {
if (audioContext.current) {
audioContext.current.close();
}
// remove all events
audioRef.current?.removeEventListener('play', () => {});
audioRef.current?.removeEventListener('pause', () => {});
audioRef.current?.removeEventListener('timeupdate', () => {});
audioRef.current?.removeEventListener('ended', () => {});
audioRef.current?.removeEventListener('canplay', () => {});
audioRef.current?.removeEventListener('loadeddata', () => {});
audioRef.current?.removeEventListener('seeked', () => {});
audioRef.current?.removeEventListener('seeking', () => {});
audioRef.current?.removeEventListener('volumechange', () => {});
audioRef.current?.removeEventListener('audioprocess', () => {});
audioRef.current?.removeEventListener('playing', () => {});
audioRef.current?.removeEventListener('loadedmetadata', () => {});
audioRef.current?.removeEventListener('ended', () => {});
audioRef.current?.removeEventListener('loadeddata', () => {});
};
}, [audioRef.current]);
return (
<audio
controls
autoPlay={autoplay}
src={props.url}
ref={audioRef}
style={{
position: 'absolute',
left: '-9999px',
opacity: 0
}}
preload="metadata"
></audio>
);
});
export default RawAudioPlayer;
+21
View File
@@ -0,0 +1,21 @@
.toolbar-wrapper {
padding: 0 24px;
color: rgba(255, 255, 255, 65%);
font-size: 16px;
background-color: rgba(0, 0, 0, 10%);
border-radius: 100px;
}
.toolbar-wrapper .anticon {
padding: 12px;
cursor: pointer;
}
.toolbar-wrapper .anticon[disabled] {
cursor: not-allowed;
opacity: 0.3;
}
.toolbar-wrapper .anticon:hover {
opacity: 0.3;
}
+103 -7
View File
@@ -1,10 +1,31 @@
import { Image as AntImage, ImageProps } from 'antd';
import fallbackImg from '@/assets/images/img_fallback.png';
import {
DownloadOutlined,
EyeOutlined,
RotateLeftOutlined,
RotateRightOutlined,
SwapOutlined,
UndoOutlined,
ZoomInOutlined,
ZoomOutOutlined
} from '@ant-design/icons';
import { Image as AntImage, ImageProps, Space } from 'antd';
import { round } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import './index.less';
const AutoImage: React.FC<ImageProps & { height: number }> = (props) => {
const { height = 100, ...rest } = props;
const [width, setWidth] = useState(0);
const AutoImage: React.FC<
ImageProps & {
height: number | string;
width?: number | string;
autoSize?: boolean;
preview?: boolean;
onLoad?: () => void;
}
> = (props) => {
const { height = 100, width: w, autoSize, preview = true, ...rest } = props;
const [width, setWidth] = useState(w || 0);
const [isError, setIsError] = useState(false);
const getImgRatio = useCallback((url: string): Promise<{ ratio: number }> => {
return new Promise((resolve) => {
@@ -20,15 +41,90 @@ const AutoImage: React.FC<ImageProps & { height: number }> = (props) => {
}, []);
const handleOnLoad = useCallback(async () => {
if (autoSize) {
return;
}
const { ratio } = await getImgRatio(props.src || '');
setWidth(height * ratio);
if (typeof height === 'number') {
setWidth(height * ratio);
} else {
throw new Error('Height must be a number');
}
}, [getImgRatio, height, props.src]);
const onDownload = useCallback(() => {
const url = props.src || '';
const filename = Date.now() + '';
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
}, [props.src]);
const handleImgLoad = useCallback(() => {
props.onLoad?.();
setIsError(false);
}, [props.onLoad]);
const handleOnError = useCallback(() => {
setIsError(true);
}, []);
useEffect(() => {
handleOnLoad();
}, [handleOnLoad]);
return <AntImage {...rest} height={height} width={width} />;
useEffect(() => {
setWidth(w || 0);
}, [w]);
return (
<AntImage
{...rest}
height={isError ? 'auto' : height}
width={isError ? '100%' : width}
onError={handleOnError}
onLoad={handleImgLoad}
fallback={fallbackImg}
crossOrigin="anonymous"
preview={
preview &&
!isError && {
mask: <EyeOutlined />,
toolbarRender: (
_,
{
transform: { scale },
actions: {
onFlipY,
onFlipX,
onRotateLeft,
onRotateRight,
onZoomOut,
onZoomIn,
onReset
}
}
) => (
<Space size={12} className="toolbar-wrapper">
<DownloadOutlined onClick={onDownload} />
<SwapOutlined rotate={90} onClick={onFlipY} />
<SwapOutlined onClick={onFlipX} />
<RotateLeftOutlined onClick={onRotateLeft} />
<RotateRightOutlined onClick={onRotateRight} />
<ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
<ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
<UndoOutlined onClick={onReset} />
</Space>
)
}
}
/>
);
};
export default AutoImage;
export default React.memo(AutoImage);
@@ -0,0 +1,44 @@
.img-wrapper {
position: relative;
display: inline-block;
}
.img-wrapper .auto-image {
display: block;
}
.img-wrapper .progress-wrapper {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
.progress-square {
width: 100%;
height: 100%;
transform: rotate(0deg);
}
.progress-square-bg {
fill: none;
}
.progress-square-fg {
fill: none;
stroke-linecap: square;
stroke-dasharray: 400;
stroke-dashoffset: 400;
transition: stroke-dashoffset 0.3s ease;
}
.progress-text {
position: absolute;
color: black;
font-size: 20px;
font-weight: bold;
}
+144
View File
@@ -0,0 +1,144 @@
.thumb-img {
position: relative;
display: flex;
max-width: 100%;
max-height: 100%;
justify-content: center;
align-items: center;
border-radius: var(--border-radius-base);
overflow: hidden;
.label {
position: absolute;
top: 4px;
left: 4px;
height: 20px;
line-height: 20px;
border-radius: 12px;
padding: 0 8px;
background-color: var(--ant-geekblue-1);
z-index: 10;
transform: scale(0.9);
}
.progress-wrapper {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
}
.small-progress-wrap {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 30%);
.ant-progress-text {
color: var(--color-white-secondary);
}
}
.img {
display: flex;
width: auto;
height: auto;
max-width: 100%;
max-height: 100%;
overflow: hidden;
border-radius: var(--border-radius-base);
cursor: pointer;
justify-content: center;
align-items: center;
}
.del {
position: absolute;
top: -4px;
right: -2px;
font-size: var(--font-size-middle);
cursor: pointer;
background-color: var(--color-white-1);
display: none;
border-radius: 50%;
height: 16px;
width: 16px;
overflow: hidden;
pointer-events: all;
}
&:hover {
.ant-image .ant-image-mask {
opacity: 1;
transition: opacity var(--ant-motion-duration-slow);
}
.del {
display: flex;
justify-content: center;
align-items: center;
}
}
}
.single-image {
height: 100%;
width: inherit;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
border-radius: var(--border-radius-base);
&.loading {
width: 100%;
height: 100%;
}
&.auto-bg-color {
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
filter: blur(100px);
backdrop-filter: blur(100px);
z-index: 5;
}
.thumb-img {
position: relative;
z-index: 10;
border-radius: 0;
.img {
border-radius: 0;
}
}
.ant-image {
border-radius: 0;
}
.mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
bottom: 0;
right: 0;
z-index: 1;
}
}
}
+199
View File
@@ -0,0 +1,199 @@
import { CloseCircleOutlined } from '@ant-design/icons';
import { Progress, ProgressProps } from 'antd';
import classNames from 'classnames';
import { round } from 'lodash';
import ResizeObserver from 'rc-resize-observer';
import React, { useCallback } from 'react';
import AutoImage from './index';
import './single-image.less';
interface SingleImageProps {
loading?: boolean;
width?: number;
height?: number;
progress?: number;
maxHeight?: number;
maxWidth?: number;
dataUrl: string;
label?: React.ReactNode;
uid: number;
preview?: boolean;
autoSize?: boolean;
onDelete: (uid: number) => void;
onClick?: (item: any) => void;
autoBgColor?: boolean;
editable?: boolean;
style?: React.CSSProperties;
loadingSize?: ProgressProps['size'];
progressType?: 'line' | 'circle' | 'dashboard';
progressColor?: string;
progressWidth?: number;
}
const SingleImage: React.FC<SingleImageProps> = (props) => {
const {
editable,
onDelete: handleOnDelete,
onClick,
autoSize,
uid,
loading,
width,
height,
progress,
maxHeight,
maxWidth,
dataUrl = '',
label,
style,
autoBgColor,
preview = true,
loadingSize = 'default'
} = props;
const imgWrapper = React.useRef<HTMLSpanElement>(null);
const [imgSize, setImgSize] = React.useState({
width: width,
height: height
});
const thumImgWrapStyle = React.useMemo(() => {
return loading ? { width: '100%', height: '100%' } : {};
}, [loading, imgSize]);
const handleOnClick = useCallback(() => {
onClick?.(props);
}, [onClick, props]);
const handleResize = useCallback(
(size: { width: number; height: number }) => {
if (!autoSize || !size.width || !size.height) return;
const { width: containerWidth, height: containerHeight } = size;
const { width: originalWidth, height: originalHeight } = props;
if (!originalWidth || !originalHeight) return;
const widthRatio = containerWidth / originalWidth;
const heightRatio = containerHeight / originalHeight;
const scale = Math.min(widthRatio, heightRatio, 1);
const newWidth = originalWidth * scale;
const newHeight = originalHeight * scale;
if (newWidth === imgSize.width && newHeight === imgSize.height) {
return;
}
setImgSize({
width: newWidth,
height: newHeight
});
},
[autoSize, props.width, props.height]
);
const handleOnLoad = React.useCallback(async () => {}, []);
return (
<ResizeObserver onResize={handleResize}>
<div
style={{ ...style }}
key={uid}
className={classNames('single-image', {
'auto-bg-color': autoBgColor,
'auto-size': autoSize,
loading: loading
})}
>
{autoBgColor && (
<div
className="mask"
style={{
background: `url(${dataUrl}) center center / cover no-repeat`
}}
></div>
)}
<span
className="thumb-img"
style={{ ...thumImgWrapStyle }}
ref={imgWrapper}
>
<>
{label && <div className="label">{label}</div>}
{loading ? (
<span
className="progress-wrap"
style={{
width: '100%',
height: '100%',
display: 'flex',
border: '1px solid var(--ant-color-split)',
borderRadius: 'var(--border-radius-base)',
justifyContent: 'center',
alignItems: 'center',
padding: '10px',
overflow: 'hidden'
}}
>
<Progress
percent={round(progress, 0)}
type="dashboard"
size={loadingSize}
steps={{ count: 50, gap: 2 }}
format={() => (
<span className="font-size-20">{round(progress, 0)}%</span>
)}
trailColor="var(--ant-color-fill-secondary)"
/>
</span>
) : (
<span
onClick={handleOnClick}
className="img"
style={{
maxHeight: `min(${maxHeight}, 100%)`,
maxWidth: `min(${maxWidth}, 100%)`
}}
>
<AutoImage
style={{ objectFit: 'cover' }}
preview={preview}
autoSize={autoSize}
src={dataUrl}
width={imgSize.width || '100%'}
height={imgSize.height || 100}
onLoad={handleOnLoad}
/>
{progress && progress < 100 && (
<span className="small-progress-wrap">
<Progress
percent={round(progress, 0)}
type="dashboard"
size="small"
steps={{ count: 25, gap: 3 }}
format={() => (
<span className="font-size-12">
{round(progress, 0)}%
</span>
)}
strokeColor="var(--color-white-secondary)"
trailColor="var(--ant-color-fill-secondary)"
/>
</span>
)}
</span>
)}
</>
{editable && (
<span className="del" onClick={() => handleOnDelete(uid)}>
<CloseCircleOutlined />
</span>
)}
</span>
</div>
</ResizeObserver>
);
};
export default SingleImage;
+53 -22
View File
@@ -1,3 +1,4 @@
import { CloseOutlined } from '@ant-design/icons';
import { Tag, Tooltip, type TagProps } from 'antd';
import { throttle } from 'lodash';
import React, {
@@ -7,7 +8,7 @@ import React, {
useRef,
useState
} from 'react';
import TitleTip from './title-tip';
import { TooltipOverlayScroller } from '../overlay-scroller';
// type TagProps = React.ComponentProps<typeof Tag>;
@@ -20,6 +21,7 @@ interface AutoTooltipProps extends Omit<TagProps, 'title'> {
ghost?: boolean;
title?: React.ReactNode;
showTitle?: boolean;
closable?: boolean;
tooltipProps?: React.ComponentProps<typeof Tooltip>;
}
@@ -35,7 +37,7 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
}) => {
const contentRef = useRef<HTMLDivElement>(null);
const [isOverflowing, setIsOverflowing] = useState(false);
const resizeObserver = useRef<ResizeObserver>();
const resizeObserver = useRef<ResizeObserver | null>(null);
const checkOverflow = useCallback(() => {
if (contentRef.current) {
@@ -44,6 +46,25 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
}
}, [contentRef.current]);
useEffect(() => {
const element = contentRef.current;
if (!element) return;
resizeObserver.current?.disconnect();
resizeObserver.current = new ResizeObserver(() => {
checkOverflow();
});
resizeObserver.current?.observe(element);
// Initial check
checkOverflow();
return () => {
resizeObserver.current?.disconnect();
resizeObserver.current = null;
};
}, [checkOverflow]);
useEffect(() => {
const debouncedCheckOverflow = throttle(checkOverflow, 200);
window.addEventListener('resize', debouncedCheckOverflow);
@@ -70,34 +91,44 @@ const AutoTooltip: React.FC<AutoTooltipProps> = ({
);
return (
<Tooltip
overlayInnerStyle={{ paddingInline: 0 }}
destroyTooltipOnHide={false}
title={
isOverflowing || showTitle ? (
<TitleTip
isOverflowing={isOverflowing}
title={title}
showTitle={showTitle}
>
{children}
</TitleTip>
) : (
''
)
}
{...tooltipProps}
<TooltipOverlayScroller
toolTipProps={{
...tooltipProps,
destroyTooltipOnHide: false
}}
title={isOverflowing || showTitle ? title || children : false}
>
{ghost ? (
<div ref={contentRef} style={tagStyle}>
<div ref={contentRef} style={tagStyle} data-overflow={isOverflowing}>
{children}
</div>
) : (
<Tag {...tagProps} ref={contentRef} style={tagStyle}>
<Tag
{...tagProps}
ref={contentRef}
style={{
...tagStyle,
paddingInline: tagProps.closable ? '8px 22px' : 8,
borderRadius: 12
}}
closeIcon={
tagProps.closable ? (
<CloseOutlined
style={{
position: 'absolute',
right: 8,
top: 6
}}
/>
) : (
false
)
}
>
{children}
</Tag>
)}
</Tooltip>
</TooltipOverlayScroller>
);
};
+9 -12
View File
@@ -1,4 +1,4 @@
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import { OverlayScroller } from '@/components/overlay-scroller';
import React from 'react';
interface TitleTipProps {
@@ -10,21 +10,18 @@ interface TitleTipProps {
const TitleTip: React.FC<TitleTipProps> = (props) => {
const { isOverflowing, showTitle, title, children } = props;
const { initialize } = useOverlayScroller();
const scrollRef = React.useRef<any>(null);
React.useEffect(() => {
if (scrollRef.current) {
initialize(scrollRef.current);
}
}, [initialize, scrollRef.current]);
return (
<div style={{ maxHeight: 200, overflowY: 'auto' }} ref={scrollRef}>
<div style={{ width: 'max-content', maxWidth: 250, paddingInline: 10 }}>
<OverlayScroller maxHeight={200}>
<div
style={{
width: 'fit-content',
maxWidth: 'var(--width-tooltip-max)'
}}
>
{isOverflowing || showTitle ? title || children : ''}
</div>
</div>
</OverlayScroller>
);
};
+46
View File
@@ -0,0 +1,46 @@
import { DoubleRightOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button } from 'antd';
import React from 'react';
import styled from 'styled-components';
interface MoreButtonProps {
show: boolean;
loadMore: () => void;
loading?: boolean;
}
const MoreWrapper = styled.div`
display: flex;
justify-content: center;
margin-block: 16px;
opacity: 1;
transition: opacity 0.3s;
&.loading {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
`;
const MoreButton: React.FC<MoreButtonProps> = (props) => {
const { show, loading, loadMore } = props;
const intl = useIntl();
return (
<>
{show ? (
<MoreWrapper className={loading ? 'loading' : ''}>
<Button
onClick={loadMore}
size="middle"
type="text"
icon={<DoubleRightOutlined rotate={90} />}
>
{intl.formatMessage({ id: 'common.button.more' })}
</Button>
</MoreWrapper>
) : null}
</>
);
};
export default MoreButton;
+1 -1
View File
@@ -3,5 +3,5 @@
background-color: var(--color-white-1);
box-shadow: none;
padding: 10px;
border: 1px solid var(--color-border-1);
border: 1px solid var(--ant-color-border);
}
+43
View File
@@ -0,0 +1,43 @@
import { Button } from 'antd';
import React from 'react';
interface CheckButtonsProps {
options: Global.BaseOption<string | number>[];
onChange: (value: string | number) => void;
cancelable?: boolean;
size?: 'small' | 'middle' | 'large';
type?: 'text' | 'primary' | 'default' | 'dashed' | 'link' | undefined;
}
const CheckButtons: React.FC<CheckButtonsProps> = (props) => {
const [type, setType] = React.useState(props.type || 'text');
const [active, setActive] = React.useState<string | number | null>(null);
const handleChange = (value: string | number) => {
props.onChange(value);
if (props.cancelable && active === value) {
setActive(null);
} else {
setActive(value);
}
};
return (
<div className="flex-center gap-6">
{props.options?.map?.((option, index) => {
return (
<Button
size={props.size}
key={option.value}
onClick={() => handleChange(option.value)}
variant="filled"
color={active === option.value ? 'default' : undefined}
type={type}
>
{option.label}
</Button>
);
})}
</div>
);
};
export default React.memo(CheckButtons);
+29 -3
View File
@@ -2,25 +2,41 @@ import { CheckCircleFilled, CopyOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tooltip, message } from 'antd';
import ClipboardJS from 'clipboard';
import { useEffect, useRef, useState } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
type CopyButtonProps = {
children?: React.ReactNode;
text: string;
disabled?: boolean;
fontSize?: string;
type?: 'text' | 'primary' | 'dashed' | 'link' | 'default';
size?: 'small' | 'middle' | 'large';
shape?: 'circle' | 'round' | 'default';
tips?: string;
placement?:
| 'top'
| 'left'
| 'right'
| 'bottom'
| 'topLeft'
| 'topRight'
| 'bottomLeft'
| 'bottomRight';
btnStyle?: React.CSSProperties;
style?: React.CSSProperties;
};
const CopyButton: React.FC<CopyButtonProps> = ({
children,
tips,
text,
disabled,
type = 'text',
shape = 'default',
fontSize = '14px',
style,
btnStyle,
placement,
size = 'middle'
}) => {
const intl = useIntl();
@@ -47,6 +63,13 @@ const CopyButton: React.FC<CopyButtonProps> = ({
}
};
const tipTitle = useMemo(() => {
if (copied) {
return intl.formatMessage({ id: 'common.button.copied' });
}
return tips ?? intl.formatMessage({ id: 'common.button.copy' });
}, [copied, intl, tips]);
useEffect(() => {
initClipboard();
return () => {
@@ -55,7 +78,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
}, [buttonRef]);
return (
<Tooltip title={intl.formatMessage({ id: 'common.button.copy' })}>
<Tooltip title={tipTitle} placement={placement}>
<Button
className="copy-button"
ref={buttonRef}
@@ -64,6 +87,7 @@ const CopyButton: React.FC<CopyButtonProps> = ({
size={size}
disabled={!!disabled}
data-clipboard-text={text}
style={{ ...btnStyle }}
icon={
copied ? (
<CheckCircleFilled
@@ -73,7 +97,9 @@ const CopyButton: React.FC<CopyButtonProps> = ({
<CopyOutlined style={{ fontSize: fontSize, ...style }} />
)
}
></Button>
>
{children}
</Button>
</Tooltip>
);
};
+85
View File
@@ -0,0 +1,85 @@
import { getStorageUserSettings } from '@/atoms/settings';
import { useEffect, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';
const duration = 1;
const fadeOut = keyframes`
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(1.05);
}
`;
const Mask = styled.div<{ isHiding: boolean }>`
display: flex;
justify-content: center;
align-items: center;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(59, 59, 59, 1);
z-index: 9999;
animation: ${({ isHiding }) => (isHiding ? fadeOut : 'none')} ${duration}s
ease forwards;
`;
const DarkModeMask = () => {
const { theme: savedTheme } = getStorageUserSettings();
const [visible, setVisible] = useState(savedTheme === 'realDark');
const [isHiding, setIsHiding] = useState(false);
const maskRef = useRef<HTMLDivElement>(null);
const timerRef = useRef<any>(null);
const initTheme = () => {
const html = document.querySelector('html');
if (html) {
html.setAttribute('data-theme', savedTheme);
}
};
initTheme();
useEffect(() => {
if (savedTheme === 'realDark') {
setVisible(true);
requestAnimationFrame(() => {
setIsHiding(true);
});
} else {
setIsHiding(true);
}
}, [savedTheme]);
useEffect(() => {
const el = maskRef.current;
if (!el) return;
const handleAnimationEnd = () => {
clearTimeout(timerRef.current);
setVisible(false);
};
el.addEventListener('animationend', handleAnimationEnd);
if (isHiding) {
timerRef.current = setTimeout(
() => {
setVisible(false);
},
duration * 1000 + 100
);
}
return () => el.removeEventListener('animationend', handleAnimationEnd);
}, [visible]);
return visible ? <Mask ref={maskRef} isHiding={isHiding}></Mask> : null;
};
export default DarkModeMask;
+94 -22
View File
@@ -1,41 +1,97 @@
import useBodyScroll from '@/hooks/use-body-scroll';
import { ExclamationCircleFilled } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Modal, Space, message, type ModalFuncProps } from 'antd';
import { forwardRef, useImperativeHandle, useState } from 'react';
import {
Button,
Checkbox,
Modal,
Space,
message,
type ModalFuncProps
} from 'antd';
import { FC, forwardRef, useImperativeHandle, useState } from 'react';
import styled from 'styled-components';
import Styles from './index.less';
const DeleteModal = forwardRef((props, ref) => {
const CheckboxWrapper = styled.div`
margin-top: 20px;
margin-left: 30px;
display: flex;
justify-content: flex-start;
align-items: center;
.check-text {
font-weight: 700;
color: var(--ant-color-warning);
}
`;
interface DeleteModalProps {
ref: any;
}
interface DataOptions {
content: string;
selection?: boolean;
name?: string;
okText?: string;
cancelText?: string;
title?: string;
operation: string;
checkConfig?: {
checkText: string;
defautlChecked: boolean;
};
}
const DeleteModal: FC<DeleteModalProps> = forwardRef((props, ref) => {
const intl = useIntl();
const { saveScrollHeight, restoreScrollHeight } = useBodyScroll();
const [visible, setVisible] = useState(false);
const [config, setConfig] = useState<
ModalFuncProps & { content: string; selection?: boolean; name?: string }
>({});
const [checked, setChecked] = useState(false);
const [config, setConfig] = useState<ModalFuncProps & DataOptions>({} as any);
useImperativeHandle(ref, () => ({
show: (
data: ModalFuncProps & {
content: string;
selection?: boolean;
name?: string;
}
) => {
show: (data: ModalFuncProps & DataOptions) => {
saveScrollHeight();
setConfig(data);
setChecked(data.checkConfig?.defautlChecked || false);
setVisible(true);
},
hide: () => {
setVisible(false);
restoreScrollHeight();
},
configuration: {
checked: checked
}
}));
const handleCancel = () => {
setVisible(false);
config.onCancel?.();
restoreScrollHeight();
};
const handleOk = async () => {
await config.onOk?.();
message.success(intl.formatMessage({ id: 'common.message.success' }));
setVisible(false);
try {
const res = await config.onOk?.();
const isArray = Array.isArray(res);
if (isArray) {
const allSuccess = res.every(
(item: any) => item?.status === 'fulfilled'
);
if (allSuccess) {
message.success(intl.formatMessage({ id: 'common.message.success' }));
}
} else {
message.success(intl.formatMessage({ id: 'common.message.success' }));
}
} catch (error) {
// Handle error if needed
} finally {
setVisible(false);
restoreScrollHeight();
}
};
return (
@@ -55,10 +111,14 @@ const DeleteModal = forwardRef((props, ref) => {
footer={
<Space size={20}>
<Button onClick={handleCancel} size="middle">
{intl.formatMessage({ id: 'common.button.cancel' })}
{config.cancelText
? intl.formatMessage({ id: config.cancelText })
: intl.formatMessage({ id: 'common.button.cancel' })}
</Button>
<Button type="primary" onClick={handleOk} size="middle">
{intl.formatMessage({ id: 'common.button.delete' })}
{config.okText
? intl.formatMessage({ id: config.okText })
: intl.formatMessage({ id: 'common.button.delete' })}
</Button>
</Space>
}
@@ -67,7 +127,9 @@ const DeleteModal = forwardRef((props, ref) => {
<span className="title">
<ExclamationCircleFilled />
<span>
{intl.formatMessage({ id: 'common.title.delete.confirm' })}
{config.title
? intl.formatMessage({ id: config.title })
: intl.formatMessage({ id: 'common.title.delete.confirm' })}
</span>
</span>
</div>
@@ -78,9 +140,7 @@ const DeleteModal = forwardRef((props, ref) => {
config.content &&
intl.formatMessage(
{
id: config.selection
? 'common.delete.confirm'
: 'common.delete.single.confirm'
id: config.operation || ''
},
{
type: intl.formatMessage({ id: config.content }),
@@ -89,6 +149,18 @@ const DeleteModal = forwardRef((props, ref) => {
)
}}
></div>
{config.checkConfig && (
<CheckboxWrapper>
<Checkbox
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
>
<span className="check-text">
{intl.formatMessage({ id: config.checkConfig?.checkText })}
</span>
</Checkbox>
</CheckboxWrapper>
)}
</Modal>
);
});
+31 -29
View File
@@ -1,34 +1,36 @@
import * as icons from '@ant-design/icons';
import { Button, Dropdown, Space } from 'antd';
import type { MenuProps } from 'antd/es/menu';
import react from 'react';
import { useIntl } from '@umijs/max';
import { Dropdown, DropDownProps } from 'antd';
import _ from 'lodash';
import React, { useMemo } from 'react';
type DropdownProps = {
trigger?: Array<'click' | 'hover' | 'contextMenu'>;
items: MenuProps['items'];
onClick: (e: any) => void;
};
const renderIcon = (icon: string) => {
if (!icon) {
return null;
}
// @ts-ignore
const Icon = icons[icon] as React.FC;
return react.createElement(Icon);
};
const DropDownActions: React.FC<DropdownProps> = (props) => {
const { trigger, items, onClick } = props;
const DropDownActions: React.FC<DropDownProps> = (props) => {
const {
menu,
trigger = ['hover'],
placement = 'bottomRight',
children,
...rest
} = props;
const intl = useIntl();
const items = useMemo(() => {
return menu?.items?.map((item: any) => ({
..._.omit(item, 'locale'),
label: item.locale ? intl.formatMessage({ id: item.label }) : item.label
}));
}, [menu?.items, intl]);
return (
<Space>
<Dropdown menu={{ items, onClick }} trigger={trigger}>
<Button
onClick={(e) => e.preventDefault()}
style={{ fontSize: '14px' }}
type="text"
icon={renderIcon('MoreOutlined')}
></Button>
</Dropdown>
</Space>
<Dropdown
menu={{
items: items,
onClick: menu?.onClick
}}
trigger={trigger}
placement={placement}
{...rest}
>
{children}
</Dropdown>
);
};
@@ -0,0 +1,4 @@
.dropdown-button.middle {
height: 28px;
width: 28px;
}
+71 -21
View File
@@ -1,23 +1,39 @@
import { MoreOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Dropdown, Tooltip, type MenuProps } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import { memo } from 'react';
import React from 'react';
import './index.less';
type Trigger = 'click' | 'hover';
interface DropdownButtonsProps {
items: MenuProps['items'];
size?: 'small' | 'middle' | 'large';
trigger?: Trigger[];
showText?: boolean;
disabled?: boolean;
variant?: 'filled' | 'outlined';
color?: string;
extra?: React.ReactNode;
onSelect: (val: any, item?: any) => void;
}
const DropdownButtons: React.FC<DropdownButtonsProps> = ({
items,
size = 'small',
size = 'middle',
trigger = ['hover'],
showText,
disabled,
variant,
color,
extra,
onSelect
}) => {
const headItem = _.head(items);
const intl = useIntl();
const handleMenuClick = (item: any) => {
console.log('menu click', item.key);
const selectItem = _.find(items, { key: item.key });
onSelect(item.key, selectItem);
};
@@ -34,23 +50,26 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
return (
<>
{items?.length === 1 ? (
<Tooltip title={intl.formatMessage({ id: _.head(items)?.label })}>
<Tooltip title={intl.formatMessage({ id: headItem?.label })}>
<Button
{..._.head(items)}
icon={_.get(items, '0.icon')}
className={classNames('dropdown-button', size)}
icon={headItem?.icon}
size={size}
{..._.get(items, '0.props')}
{...headItem?.props}
onClick={handleButtonClick}
></Button>
</Tooltip>
) : (
<Dropdown.Button
disabled={disabled}
trigger={trigger}
type="primary"
dropdownRender={(menus: any) => {
return (
<div
className="flex flex-column "
style={{
backgroundColor: 'var(--color-white-1)',
backgroundColor: 'var(--ant-color-bg-elevated)',
padding: 5,
alignItems: 'flex-start',
borderRadius: 'var(--border-radius-base)',
@@ -62,9 +81,10 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
<Button
{...item.props}
type="text"
size="middle"
size={size}
icon={item.icon}
key={item.key}
disabled={item.disabled}
onClick={() => handleMenuClick(item)}
style={{ width: '100%', justifyContent: 'flex-start' }}
>
@@ -76,17 +96,47 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
);
}}
buttonsRender={([leftButton, rightButton]) => [
<Tooltip
title={intl.formatMessage({ id: _.head(items)?.label })}
key="leftButton"
>
<Button
onClick={handleButtonClick}
size={size}
icon={_.head(items)?.icon}
></Button>
</Tooltip>,
<Button icon={<MoreOutlined />} size={size} key="menu"></Button>
<>
{showText ? (
<Button
{...headItem?.props}
disabled={headItem?.disabled || disabled}
className={classNames('dropdown-button', size)}
onClick={handleButtonClick}
size={size}
icon={headItem?.icon}
variant={variant}
color={color}
>
{intl.formatMessage({
id: headItem?.label
})}
{extra}
</Button>
) : (
<Tooltip
title={intl.formatMessage({ id: headItem?.label })}
key="leftButton"
>
<Button
{...headItem?.props}
className={classNames('dropdown-button', size)}
onClick={handleButtonClick}
size={size}
icon={headItem?.icon}
disabled={headItem?.disabled}
></Button>
</Tooltip>
)}
</>,
<Button
icon={<MoreOutlined />}
size={size}
key="menu"
variant={variant}
color="default"
className={classNames('dropdown-button', size)}
></Button>
]}
></Dropdown.Button>
)}
@@ -94,4 +144,4 @@ const DropdownButtons: React.FC<DropdownButtonsProps> = ({
);
};
export default memo(DropdownButtons);
export default DropdownButtons;
+58 -49
View File
@@ -1,16 +1,8 @@
import Chart from '@/components/echarts/chart';
import {
barItemConfig,
grid,
legend,
title as titleConfig,
tooltip,
xAxis,
yAxis
} from '@/components/echarts/config';
import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data';
import _ from 'lodash';
import { memo } from 'react';
import React, { memo, useMemo } from 'react';
import { ChartProps } from './types';
const BarChart: React.FC<ChartProps> = (props) => {
@@ -23,39 +15,41 @@ const BarChart: React.FC<ChartProps> = (props) => {
legendData,
title
} = props;
if (!seriesData.length) {
return <EmptyData height={height} title={title}></EmptyData>;
}
const options = {
title: {
text: ''
},
const {
barItemConfig,
grid,
tooltip: {
...tooltip
// axisPointer: {
// type: 'shadow'
// }
},
xAxis: {
...xAxis,
axisLabel: {
...xAxis.axisLabel,
formatter: labelFormatter
},
data: []
},
yAxis,
legend: {
...legend,
data: []
},
legend,
title: titleConfig,
tooltip,
xAxis,
yAxis
} = useChartConfig();
series: []
};
const setDataOptions = () => {
const dataOptions = useMemo((): any => {
const options = {
title: {
text: ''
},
grid,
tooltip: {
...tooltip
},
xAxis: {
...xAxis,
axisLabel: {
...xAxis.axisLabel,
formatter: labelFormatter
},
data: []
},
yAxis,
legend: {
...legend,
data: []
},
series: []
};
const data = _.map(seriesData, (item: any) => {
return {
...item,
@@ -82,16 +76,31 @@ const BarChart: React.FC<ChartProps> = (props) => {
},
series: data
};
};
const dataOptions: any = setDataOptions();
}, [
seriesData,
xAxisData,
title,
labelFormatter,
tooltip,
grid,
xAxis,
yAxis,
legend,
barItemConfig
]);
return (
<Chart
height={height}
options={dataOptions}
width={width || '100%'}
></Chart>
<>
{!seriesData.length ? (
<EmptyData height={height} title={title}></EmptyData>
) : (
<Chart
height={height}
options={dataOptions}
width={width || '100%'}
></Chart>
)}
</>
);
};
+75
View File
@@ -0,0 +1,75 @@
import React from 'react';
import styled from 'styled-components';
const TooltipWrapper = styled.div`
display: flex;
flex-direction: column;
gap: 4px;
font-size: 11px;
background-color: rgba(255, 255, 255, 80%);
min-width: 100px;
max-width: 360px;
.tooltip-x-name {
font-size: var(--font-size-base);
color: var(--ant-color-text-tertiary);
}
.tooltip-item {
color: var(--ant-color-text-secondary);
display: flex;
justify-content: space-between;
align-items: center;
.tooltip-item-title {
margin-right: 2px;
}
.tooltip-value {
margin-left: 10px;
color: var(--ant-color-text);
text-overflow: ellipsis;
overflow: hidden;
}
}
`;
const ItemSymbol = styled.span<{ $color: string }>`
background-color: ${(props) => props.$color};
display: inline-block;
marginright: 5px;
borderradius: 8px;
width: 8px;
height: 8px;
`;
interface ChartTooltipProps {
params: any[];
callback?: (val: any) => any;
}
const ChartTooltip: React.FC<ChartTooltipProps> = (props) => {
const { params, callback } = props;
console.log('params====', params);
return (
<TooltipWrapper>
<span className="tooltip-x-name">{params[0]?.axisValue}</span>
<>
{params.map((item: any, index: number) => {
let value = callback?.(item.data.value) || item.data.value;
return (
<span className="tooltip-item" key={index}>
<span className="tooltip-item-name">
<ItemSymbol $color={item.color}></ItemSymbol>
<span className="tooltip-title">{item.seriesName}</span>:
</span>
<span className="tooltip-value">{value}</span>
</span>
);
})}
</>
</TooltipWrapper>
);
};
export default ChartTooltip;
+32 -21
View File
@@ -1,17 +1,30 @@
import { throttle } from 'lodash';
import { useCallback, useEffect, useRef } from 'react';
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useRef
} from 'react';
import echarts, { ECOption } from '.';
const Chart: React.FC<{
options: ECOption;
height: number | string;
width: number | string;
}> = ({ options, width, height }) => {
ref?: any;
}> = forwardRef(({ options, width, height }, ref) => {
const container = useRef<HTMLDivElement>(null);
const chart = useRef<echarts.EChartsType>();
const resizeable = useRef(false);
const resizeObserver = useRef<ResizeObserver>();
useImperativeHandle(ref, () => {
return {
chart: chart.current
};
});
const init = useCallback(() => {
if (container.current) {
chart.current?.clear();
@@ -23,13 +36,16 @@ const Chart: React.FC<{
chart.current?.resize();
}, []);
const setOption = useCallback((options: ECOption) => {
chart.current?.clear();
chart.current?.setOption(options, {
notMerge: true,
lazyUpdate: true
});
}, []);
const setOption = useCallback(
(options: ECOption) => {
chart.current?.clear();
chart.current?.setOption(options, {
notMerge: true,
lazyUpdate: true
});
},
[options]
);
useEffect(() => {
if (container.current) {
@@ -44,18 +60,9 @@ const Chart: React.FC<{
resizeable.current = false;
resize();
setOption(options);
resizeable.current = true;
}, [options]);
useEffect(() => {
let timer: any = null;
timer = setTimeout(() => {
resizeable.current = true;
}, 300);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
const handleResize = throttle(() => {
if (resizeable.current) {
@@ -75,7 +82,11 @@ const Chart: React.FC<{
};
}, []);
return <div ref={container} style={{ width: width, height }}></div>;
};
return (
<div className="chart-wrapper" style={{ width: width, height }}>
<div ref={container} style={{ width: width, height }}></div>
</div>
);
});
export default Chart;
+226 -169
View File
@@ -1,29 +1,21 @@
import useUserSettings from '@/hooks/use-user-settings';
import { theme } from 'antd';
import { isFunction } from 'lodash';
const chartColorMap = {
tickLineColor: 'rgba(217,217,217,0.5)',
axislabelColor: 'rgba(0, 0, 0, 0.4)'
};
import { useMemo } from 'react';
export const tooltip = {
trigger: 'axis',
// axisPointer: {
// type: 'shadow'
// }
formatter(params: any, callback?: (val: any) => any) {
let result = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
params.forEach((item: any) => {
let value = isFunction(callback)
? callback?.(item.data.value)
: item.data.value;
result += `<span class="tooltip-item">
<span class="tooltip-item-name">
<span style="display:inline-block;margin-right:5px;border-radius:8px;width:8px;height:8px;background-color:${item.color};"></span>
<span class="tooltip-title">${item.seriesName}</span>:
</span>
<span class="tooltip-value">${value}</span>
</span>`;
});
return `<div class="tooltip-wrapper">${result}</div>`;
const formatLargeNumber = (value: number) => {
if (typeof value !== 'number' || isNaN(value)) {
return value;
}
if (value >= 1e9) {
return (value / 1e9).toFixed(1).replace(/\.0$/, '') + 'B';
} else if (value >= 1e6) {
return (value / 1e6).toFixed(1).replace(/\.0$/, '') + 'M';
} else if (value >= 1e3) {
return (value / 1e3).toFixed(1).replace(/\.0$/, '') + 'K';
} else {
return value;
}
};
@@ -34,157 +26,222 @@ export const grid = {
containLabel: true
};
export const legend = {
itemWidth: 8,
itemHeight: 8
};
export default function useChartConfig() {
const { userSettings, isDarkTheme } = useUserSettings();
const { useToken } = theme;
const { token } = useToken();
export const xAxis = {
type: 'category',
axisTick: {
show: true,
lineStyle: {
color: chartColorMap.tickLineColor
}
},
axisLabel: {
color: chartColorMap.axislabelColor,
fontSize: 12
},
axisLine: {
show: false
}
};
console.log('token:', token);
export const yAxis = {
// max: 100,
// min: 0,
nameTextStyle: {
padding: [0, 0, 0, -20]
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
},
axisLabel: {
color: chartColorMap.axislabelColor,
fontSize: 12
},
axisTick: {
show: false
},
type: 'value'
};
const chartColorMap = useMemo(() => {
return {
titleColor: token.colorText,
splitLineColor: token.colorBorder,
tickLineColor: token.colorSplit,
axislabelColor: token.colorTextTertiary,
gaugeBgColor: token.colorFillSecondary,
gaugeSplitLineColor: isDarkTheme
? 'rgba(255,255,255,.3)'
: 'rgba(255, 255, 255, 1)',
gaugeSplitLineColor2: isDarkTheme
? 'rgba(255,255,255,.5)'
: 'rgba(255, 255, 255, 1)',
colorBgContainerHover: isDarkTheme ? '#424242' : '#fff'
};
}, [userSettings.theme, isDarkTheme]);
export const title = {
show: true,
left: 'center',
textStyle: {
fontSize: 12,
color: '#000'
},
text: ''
};
const tooltip = {
trigger: 'axis',
backgroundColor: chartColorMap.colorBgContainerHover,
borderColor: 'transparent',
formatter(params: any, callback?: (val: any) => any) {
let result = `<span class="tooltip-x-name">${params[0].axisValue}</span>`;
params.forEach((item: any) => {
let value = isFunction(callback)
? callback?.(item.data.value)
: item.data.value;
result += `<span class="tooltip-item">
<span class="tooltip-item-name">
<span style="display:inline-block;margin-right:5px;border-radius:8px;width:8px;height:8px;background-color:${item.color};"></span>
<span class="tooltip-title">${item.seriesName}</span>:
</span>
<span class="tooltip-value">${value}</span>
</span>`;
});
return `<div class="tooltip-wrapper">${result}</div>`;
}
};
export const barItemConfig = {
type: 'bar',
barMaxWidth: 20,
barMinWidth: 8,
// stack: 'total',
barGap: '30%',
barCategoryGap: '50%'
};
const legend = {
itemWidth: 8,
itemHeight: 8,
textStyle: {
color: chartColorMap.axislabelColor
}
};
export const lineItemConfig = {
type: 'line',
smooth: true,
showSymbol: false,
itemStyle: {},
lineStyle: {
width: 1.5,
opacity: 0.7
}
};
export const gaugeItemConfig = {
type: 'gauge',
radius: '88%',
center: ['50%', '65%'],
startAngle: 190,
endAngle: -10,
min: 0,
max: 100,
splitNumber: 5,
progress: {
show: true,
roundCap: false,
width: 12
},
pointer: {
length: '80%',
width: 4,
itemStyle: {
color: 'auto'
}
},
axisLine: {
roundCap: false,
lineStyle: {
width: 12,
// color: [[1, 'rgba(221, 221, 221, 0.5)']],
color: [
[0.5, 'rgba(84, 204, 152, 80%)'],
[0.8, 'rgba(250, 173, 20, 80%)'],
[1, 'rgba(255, 77, 79, 80%)']
]
}
},
axisTick: {
distance: -12,
length: 6,
splitNumber: 5,
lineStyle: {
width: 1.5,
color: 'rgba(255, 255, 255, 1)'
}
},
splitLine: {
distance: -12,
length: 12,
lineStyle: {
width: 1.5,
color: 'rgba(255, 255, 255, 1)'
}
},
axisLabel: {
distance: 14,
color: 'rgba(0, 0, 0, .5)',
fontSize: 12
},
detail: {
lineHeight: 40,
height: 40,
offsetCenter: [5, 30],
valueAnimation: false,
fontSize: 20,
color: 'rgba(0, 0, 0, 0.88)',
formatter(value: any) {
return '{value|' + value + '}{unit|%}';
const xAxis = {
type: 'category',
axisTick: {
show: true,
lineStyle: {
color: chartColorMap.tickLineColor
}
},
rich: {
value: {
fontSize: 16,
fontWeight: '500',
color: 'rgba(0, 0, 0, 0.88)'
axisLabel: {
color: chartColorMap.axislabelColor,
fontSize: 12
},
axisLine: {
show: false
}
};
const yAxis = {
// max: 100,
// min: 0,
nameTextStyle: {
padding: [0, 0, 0, -20]
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: chartColorMap.splitLineColor
}
},
axisLabel: {
color: chartColorMap.axislabelColor,
fontSize: 12,
formatter: formatLargeNumber
},
axisTick: {
show: false
},
type: 'value'
};
const title = {
show: true,
left: 'center',
textStyle: {
fontSize: 12,
color: chartColorMap.titleColor
},
text: ''
};
const barItemConfig = {
type: 'bar',
barMaxWidth: 20,
barMinWidth: 8,
// stack: 'total',
barGap: '30%',
barCategoryGap: '50%'
};
const lineItemConfig = {
type: 'line',
smooth: true,
showSymbol: false,
itemStyle: {},
lineStyle: {
width: 1.5,
opacity: 0.7
}
};
const gaugeItemConfig = {
type: 'gauge',
radius: '88%',
center: ['50%', '65%'],
startAngle: 190,
endAngle: -10,
min: 0,
max: 100,
splitNumber: 5,
progress: {
show: true,
roundCap: false,
width: 12
},
pointer: {
length: '80%',
width: 4,
itemStyle: {
color: 'auto'
}
},
axisLine: {
roundCap: false,
lineStyle: {
width: 12,
color: [
[0.5, 'rgba(84, 204, 152, 80%)'],
[0.8, 'rgba(250, 173, 20, 80%)'],
[1, 'rgba(255, 77, 79, 80%)']
]
}
},
axisTick: {
distance: -11,
length: 6,
splitNumber: 5,
lineStyle: {
width: 1.5,
color: chartColorMap.gaugeSplitLineColor
}
},
splitLine: {
distance: -5,
length: 5,
lineStyle: {
width: 1.5,
color: chartColorMap.gaugeSplitLineColor2
}
},
axisLabel: {
distance: 14,
color: chartColorMap.axislabelColor,
fontSize: 12
},
detail: {
lineHeight: 40,
height: 40,
offsetCenter: [5, 30],
valueAnimation: false,
fontSize: 20,
color: chartColorMap.titleColor,
formatter(value: any) {
return '{value|' + value + '}{unit|%}';
},
unit: {
fontSize: 14,
color: 'rgba(0, 0, 0, 0.88)',
fontWeight: '500',
padding: [0, 0, 0, 2]
rich: {
value: {
fontSize: 16,
fontWeight: '500',
color: chartColorMap.titleColor
},
unit: {
fontSize: 14,
color: chartColorMap.titleColor,
fontWeight: '500',
padding: [0, 0, 0, 2]
}
}
}
}
};
};
return {
tooltip,
grid,
legend,
xAxis,
yAxis,
title,
chartColorMap,
barItemConfig,
lineItemConfig,
gaugeItemConfig,
isDark: isDarkTheme
};
}
+8 -6
View File
@@ -1,15 +1,17 @@
import Chart from '@/components/echarts/chart';
import {
gaugeItemConfig,
title as titleConfig
} from '@/components/echarts/config';
import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data';
import { memo } from 'react';
import React, { memo } from 'react';
import { ChartProps } from './types';
const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
props
) => {
const {
gaugeItemConfig,
title: titleConfig,
chartColorMap
} = useChartConfig();
const { value, height, width, labelFormatter, title, color } = props;
if (!value && value !== 0) {
return <EmptyData height={height} title={title}></EmptyData>;
@@ -32,7 +34,7 @@ const GaugeChart: React.FC<Omit<ChartProps, 'seriesData' | 'xAxisData'>> = (
...gaugeItemConfig.axisLine.lineStyle,
color: [
[value / 100, color],
[1, 'rgba(221, 221, 221, 0.5)']
[1, chartColorMap.gaugeBgColor]
]
}
},
+55 -42
View File
@@ -1,15 +1,8 @@
import Chart from '@/components/echarts/chart';
import {
grid,
legend,
title as titleConfig,
tooltip,
xAxis,
yAxis
} from '@/components/echarts/config';
import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data';
import _ from 'lodash';
import { memo } from 'react';
import React, { memo, useMemo } from 'react';
import { ChartProps } from './types';
const BarChart: React.FC<ChartProps> = (props) => {
@@ -22,33 +15,40 @@ const BarChart: React.FC<ChartProps> = (props) => {
legendData,
title
} = props;
if (!seriesData.length) {
return <EmptyData height={height} title={title}></EmptyData>;
}
const options = {
title: titleConfig,
const {
grid,
tooltip: {
...tooltip
},
xAxis: {
...xAxis,
axisLabel: {
...xAxis.axisLabel,
formatter: labelFormatter
}
},
yAxis,
legend: {
...legend,
data: []
},
legend,
title: titleConfig,
tooltip,
xAxis,
yAxis
} = useChartConfig();
series: []
};
const dataOptions = useMemo((): any => {
const options = {
title: {
...titleConfig,
left: 'start'
},
grid,
tooltip: {
...tooltip
},
xAxis: {
...xAxis,
axisLabel: {
...xAxis.axisLabel,
formatter: labelFormatter
}
},
yAxis,
legend: {
...legend,
data: []
},
const setDataOptions = () => {
series: []
};
const data = _.map(seriesData, (item: any) => {
return {
...item,
@@ -69,7 +69,6 @@ const BarChart: React.FC<ChartProps> = (props) => {
animation: false,
title: {
...options.title,
left: 'start',
text: title
},
yAxis: {
@@ -103,16 +102,30 @@ const BarChart: React.FC<ChartProps> = (props) => {
},
series: data
};
};
const dataOptions: any = setDataOptions();
}, [
seriesData,
xAxisData,
title,
labelFormatter,
tooltip,
grid,
xAxis,
yAxis,
legend
]);
return (
<Chart
height={height}
options={dataOptions}
width={width || '100%'}
></Chart>
<>
{!seriesData.length ? (
<EmptyData height={height} title={title}></EmptyData>
) : (
<Chart
height={height}
options={dataOptions}
width={width || '100%'}
></Chart>
)}
</>
);
};
+7 -2
View File
@@ -1,9 +1,10 @@
import type {
BarSeriesOption,
GaugeSeriesOption,
LineSeriesOption
LineSeriesOption,
ScatterSeriesOption
} from 'echarts/charts';
import { BarChart, GaugeChart, LineChart } from 'echarts/charts';
import { BarChart, GaugeChart, LineChart, ScatterChart } from 'echarts/charts';
import type {
DatasetComponentOption,
GridComponentOption,
@@ -11,6 +12,7 @@ import type {
TooltipComponentOption
} from 'echarts/components';
import {
DataZoomComponent,
DatasetComponent,
GridComponent,
LegendComponent,
@@ -32,6 +34,7 @@ type ECOption = ComposeOption<
| GridComponentOption
| DatasetComponentOption
| GaugeSeriesOption
| ScatterSeriesOption
>;
// register components and charts
@@ -42,8 +45,10 @@ echarts.use([
GridComponent,
DatasetComponent,
TransformComponent,
DataZoomComponent,
BarChart,
LineChart,
ScatterChart,
GaugeChart,
LabelLayout,
UniversalTransition,
+37 -25
View File
@@ -1,16 +1,8 @@
import Chart from '@/components/echarts/chart';
import {
grid,
legend,
lineItemConfig,
title as titleConfig,
tooltip,
xAxis,
yAxis
} from '@/components/echarts/config';
import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data';
import _ from 'lodash';
import { memo } from 'react';
import React, { memo, useMemo } from 'react';
import { ChartProps } from './types';
const LineChart: React.FC<ChartProps> = (props) => {
@@ -26,9 +18,15 @@ const LineChart: React.FC<ChartProps> = (props) => {
smooth,
title
} = props;
if (!seriesData.length) {
return <EmptyData height={height} title={title}></EmptyData>;
}
const {
grid,
legend,
lineItemConfig,
title: titleConfig,
tooltip,
xAxis,
yAxis
} = useChartConfig();
const options = {
title: {
@@ -53,13 +51,18 @@ const LineChart: React.FC<ChartProps> = (props) => {
yAxis,
legend: {
...legend,
data: legendData
data: legendData.map((item: any) => {
return {
name: item,
icon: 'circle'
};
})
},
series: []
};
const generateOptions = (): any => {
const dataOptions = useMemo((): any => {
const data = _.map(seriesData, (item: any) => {
return {
...item,
@@ -75,7 +78,7 @@ const LineChart: React.FC<ChartProps> = (props) => {
}
};
});
const optionsConfig = {
return {
...options,
animation: false,
title: {
@@ -84,7 +87,11 @@ const LineChart: React.FC<ChartProps> = (props) => {
},
yAxis: {
...options.yAxis,
name: yAxisName
name: yAxisName,
nameTextStyle: {
fontSize: 12,
align: 'right'
}
},
xAxis: {
...options.xAxis,
@@ -92,15 +99,20 @@ const LineChart: React.FC<ChartProps> = (props) => {
},
series: data
};
return optionsConfig;
};
const dataOptions = generateOptions();
}, [seriesData, xAxisData, yAxisName, title, smooth, legendData, options]);
return (
<Chart
height={height}
options={dataOptions}
width={width || '100%'}
></Chart>
<>
{!seriesData.length ? (
<EmptyData height={height} title={title}></EmptyData>
) : (
<Chart
height={height}
options={dataOptions}
width={width || '100%'}
></Chart>
)}
</>
);
};
+217
View File
@@ -0,0 +1,217 @@
import Chart from '@/components/echarts/chart';
import useChartConfig from '@/components/echarts/config';
import EmptyData from '@/components/empty-data';
import _ from 'lodash';
import React, { memo, useCallback, useMemo, useRef } from 'react';
import { ChartProps } from './types';
const Scatter: React.FC<ChartProps> = (props) => {
const { grid, title: titleConfig, isDark, chartColorMap } = useChartConfig();
const { seriesData, xAxisData, height, width, showEmpty, title } = props;
const chart = useRef<any>(null);
const options = useMemo(() => {
const colorMap = isDark
? {
split: chartColorMap.splitLineColor,
axis: chartColorMap.axislabelColor,
label: chartColorMap.axislabelColor
}
: {
split: '#F2F2F2',
axis: '#dcdcdc',
label: '#dcdcdc'
};
return {
animation: false,
grid: {
...grid,
right: 10,
top: 10,
bottom: 2,
left: 2,
containLabel: true,
borderRadius: 4
},
xAxis: {
min: -1,
max: 1,
scale: false,
slient: true,
splitNumber: 15,
splitLine: {
lineStyle: {
color: colorMap.split
}
},
axisLine: {
show: true,
lineStyle: {
color: colorMap.axis
}
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: colorMap.label
},
boundaryGap: [0.05, 0.05]
},
yAxis: {
min: -1,
max: 1,
scale: false,
slient: true,
splitNumber: 10,
boundaryGap: [0.05, 0.05],
splitLine: {
lineStyle: {
color: colorMap.split
}
},
axisLine: {
show: true,
lineStyle: {
color: colorMap.axis
}
},
axisTick: {
show: false
},
axisLabel: {
show: true,
color: colorMap.label
}
},
symbol: 'roundRect',
label: {
show: true,
shadowColor: 'none',
textBorderColor: 'none',
formatter: (params: any) => {
return params.name;
}
},
series: []
};
}, [isDark]);
const findOverlappingPoints = useCallback(
(data: any[], currentPoint: any) => {
const overlappingPoints = [];
const symbolRadius = 16;
const [x1, y1] = chart.current.chart?.convertToPixel(
'grid',
currentPoint.value
);
const pixelPoints = data.map((point) => {
return {
...point,
value: chart.current.chart?.convertToPixel('grid', point.value)
};
});
for (let j = 0; j < pixelPoints.length; j++) {
if (currentPoint.name === pixelPoints[j].name) {
overlappingPoints.push({ ...pixelPoints[j] });
continue;
}
const [x2, y2] = pixelPoints[j].value;
const distance = Math.sqrt(
Math.pow(_.round(x2 - x1, 2), 2) + Math.pow(_.round(y2 - y1, 2), 2)
);
if (distance <= symbolRadius) {
overlappingPoints.push({ ...pixelPoints[j] });
}
}
return overlappingPoints;
},
[]
);
const renderNameInTooltip = useCallback((dataList: any[]) => {
if (!dataList.length || dataList.length < 2) {
return null;
}
const renderText = (item: any) => {
return `<span class="tooltip-item-name">
<span style="display:flex;justify-content:center;align-items: center;color:#fff;
margin-right:0;border-radius:4px;width:14px;
height:14px;background-color:${item?.itemStyle?.color};"
>${item.name}</span>
</span>`;
};
return renderText;
}, []);
const dataOptions = useMemo((): any => {
const seriseDataList = seriesData.map((item: any, index: number) => {
return {
...item,
itemStyle: {
color: '#5470c6'
},
symbolSize: 16
};
});
return {
...options,
tooltip: {
trigger: 'item',
borderWidth: 0,
backgroundColor: chartColorMap.colorBgContainerHover,
borderColor: 'transparent',
formatter(params: any, callback?: (val: any) => any) {
const dataList = findOverlappingPoints(seriseDataList, params.data);
let result = '';
const renderText: any = renderNameInTooltip(dataList);
dataList.forEach((item: any) => {
result += `
<span class="tooltip-item" style="justify-content: flex-start;">
${renderText ? renderText(item) : ''}
<span class="tooltip-value">${item.text}</span>
</span>`;
});
return `<div class="tooltip-wrapper">${result}</div>`;
}
},
title: {
...titleConfig,
text: title
},
series: {
type: 'scatter',
labelLayout: {
hideOverlap: true
},
data: seriseDataList
}
};
}, [seriesData, xAxisData, title, options, findOverlappingPoints]);
return (
<>
{!seriesData.length && showEmpty ? (
<EmptyData height={height} title={title}></EmptyData>
) : (
<Chart
ref={chart}
height={height}
options={dataOptions}
width={width || '100%'}
></Chart>
)}
</>
);
};
export default memo(Scatter);
+2 -1
View File
@@ -1,9 +1,10 @@
export interface ChartProps {
seriesData: any[];
showEmpty?: boolean;
xAxisData: string[];
legendData?: string[];
labelFormatter?: (val?: any) => string;
tooltipValueFormatter?: (val?: any) => string;
tooltipValueFormatter?: (val: any) => string;
height: string | number;
width?: string | number;
title?: string;
+3 -18
View File
@@ -9,28 +9,13 @@
.editor-header {
display: flex;
height: 40px;
padding: 0 10px;
padding-block: 0;
padding-inline: 12px 10px;
justify-content: space-between;
align-items: center;
background-color: rgb(56, 56, 56);
.ant-select-selector {
background-color: rgba(255, 255, 255, 9%) !important;
color: rgba(255, 255, 255, 70%);
border-radius: var(--border-radius-2px);
.ant-select-selection-item {
color: rgba(255, 255, 255, 70%);
}
}
.ant-select-arrow {
color: rgba(255, 255, 255, 70%);
}
background-color: var(--color-editor-header-bg);
}
// set scrollbar style
.scrollbar {
.slider {
border-radius: 6px;
+18 -12
View File
@@ -1,33 +1,40 @@
import { Select } from 'antd';
import React from 'react';
import styled from 'styled-components';
import CopyButton from '../copy-button';
import SegmentLine from '../segment-line';
import './index.less';
const Wrapper = styled.div<{ $height: number }>`
height: ${(props) => props.$height}px;
`;
interface EditorwrapProps {
headerHeight?: number;
header?: React.ReactNode;
children: React.ReactNode;
showHeader?: boolean;
copyText: string;
defaultValue?: string;
langOptions?: { label: string; value: string }[];
langOptions?: Global.BaseOption<string | number>[];
styles?: {
wrapper?: React.CSSProperties;
header?: React.CSSProperties;
content?: React.CSSProperties;
};
onChangeLang?: (value: string) => void;
onChangeLang?: (value: string | number) => void;
}
const EditorWrap: React.FC<EditorwrapProps> = ({
headerHeight = 40,
header,
children,
copyText,
langOptions,
langOptions = [],
onChangeLang,
defaultValue,
styles = {},
showHeader = true
}) => {
const handleChangeLang = (value: string) => {
const handleChangeLang = (value: string | number) => {
onChangeLang?.(value);
};
const renderHeader = () => {
@@ -36,15 +43,14 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
}
if (showHeader) {
return (
<div className="editor-header">
<Select
<Wrapper className="editor-header" $height={headerHeight}>
<SegmentLine
height={headerHeight}
defaultValue={defaultValue}
style={{ width: 120 }}
size="middle"
variant="filled"
size="small"
options={langOptions}
onChange={handleChangeLang}
></Select>
></SegmentLine>
<CopyButton
text={copyText}
size="small"
@@ -52,7 +58,7 @@ const EditorWrap: React.FC<EditorwrapProps> = ({
color: 'rgba(255,255,255,.7)'
}}
/>
</div>
</Wrapper>
);
}
return null;
-16
View File
@@ -1,16 +0,0 @@
// footer all styles
.footer {
bottom: 0;
width: 100%;
background-color: transparent;
padding: 20px 0;
text-align: center;
font-size: var(--font-size-middle);
color: var(--color-text-2);
.footer-content-left-text {
display: flex;
justify-content: center;
align-items: center;
}
}
+45 -15
View File
@@ -3,14 +3,41 @@ import { getAtomStorage } from '@/atoms/utils';
import VersionInfo, { modalConfig } from '@/components/version-info';
import externalLinks from '@/constants/external-links';
import { useIntl } from '@umijs/max';
import { Button, Modal, Space } from 'antd';
import './index.less';
import { Button, Divider, Modal } from 'antd';
import { createStyles } from 'antd-style';
import styled from 'styled-components';
const CompanyWrapper = styled.div`
display: flex;
align-items: center;
gap: 4px;
padding-inline: 0 8px;
`;
const useStyles = createStyles(({ token, css }) => ({
footer: css`
bottom: 0;
width: 100%;
background-color: transparent;
padding: 20px 0;
text-align: center;
font-size: var(--font-size-middle);
color: ${token.colorTextTertiary};
`,
'footer-content-left-text': {
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}
}));
const Footer: React.FC = () => {
const intl = useIntl();
const [modal, contextHolder] = Modal.useModal();
const { styles } = useStyles();
const showVersion = () => {
Modal.info({
modal.info({
...modalConfig,
width: 460,
content: <VersionInfo intl={intl} />
@@ -18,16 +45,18 @@ const Footer: React.FC = () => {
};
return (
<div className="footer">
<div className="footer-content">
<div className="footer-content-left">
<div className="footer-content-left-text">
<Space size={4}>
<span>&copy;</span>
<span> {new Date().getFullYear()}</span>
<span> {intl.formatMessage({ id: 'settings.company' })}</span>
</Space>
<Space size={8} style={{ marginLeft: 18 }}>
<>
{contextHolder}
<div className={styles.footer}>
<div className="footer-content">
<div className="footer-content-left">
<div className={styles['footer-content-left-text']}>
<CompanyWrapper>
<span>&copy;</span>
<span> {new Date().getFullYear()}</span>
<span> {intl.formatMessage({ id: 'settings.company' })}</span>
</CompanyWrapper>
<Divider type="vertical" />
<Button
type="link"
size="small"
@@ -36,14 +65,15 @@ const Footer: React.FC = () => {
>
{intl.formatMessage({ id: 'common.button.help' })}
</Button>
<Divider type="vertical" />
<Button type="link" size="small" onClick={showVersion}>
{getAtomStorage(GPUStackVersionAtom)?.version}
</Button>
</Space>
</div>
</div>
</div>
</div>
</div>
</>
);
};
@@ -4,15 +4,18 @@ import './styles/dark.less';
interface CodeViewerProps {
code: string;
copyValue?: string;
lang: string;
autodetect?: boolean;
ignoreIllegals?: boolean;
copyable?: boolean;
height?: string | number;
style?: React.CSSProperties;
}
const DarkViewer: React.FC<CodeViewerProps> = (props) => {
const {
code,
copyValue,
lang,
autodetect,
ignoreIllegals,
@@ -22,8 +25,10 @@ const DarkViewer: React.FC<CodeViewerProps> = (props) => {
return (
<CodeViewer
style={props.style}
height={height}
code={code}
copyValue={copyValue}
lang={lang}
theme="dark"
autodetect={autodetect}
@@ -4,26 +4,32 @@ import './styles/light.less';
interface CodeViewerProps {
code: string;
copyValue?: string;
lang: string;
autodetect?: boolean;
ignoreIllegals?: boolean;
copyable?: boolean;
height?: string | number;
style?: React.CSSProperties;
}
const LightViewer: React.FC<CodeViewerProps> = (props) => {
const {
code,
copyValue,
lang,
autodetect,
ignoreIllegals,
copyable,
style,
height = 'auto'
} = props || {};
return (
<CodeViewer
style={style}
height={height}
code={code}
copyValue={copyValue}
lang={lang}
theme="light"
autodetect={autodetect}
+94 -32
View File
@@ -1,26 +1,86 @@
import classNames from 'classnames';
import hljs from 'highlight.js';
import { memo, useMemo } from 'react';
import styled from 'styled-components';
import CopyButton from '../copy-button';
import { escapeHtml } from './utils';
interface CodeViewerProps {
code: string;
copyValue?: string;
lang: string;
autodetect?: boolean;
ignoreIllegals?: boolean;
copyable?: boolean;
height?: string | number;
theme?: 'light' | 'dark';
style?: React.CSSProperties;
}
interface CodeHeaderProps {
copyValue: string;
copyable: boolean;
lang: string;
theme: 'light' | 'dark';
}
const CodeHeaderWrapper = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
height: 32px;
padding: 0 12px;
font-size: 12px;
color: var(--ant-color-text-tertiary);
background-color: #fafafa;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
&.dark {
background-color: var(--color-editor-header-bg);
color: rgba(255, 255, 255, 0.65);
}
`;
const Wrapper = styled.div`
border-radius: var(--border-radius-mini);
`;
const CodeHeader: React.FC<CodeHeaderProps> = ({
copyValue,
lang,
theme,
copyable
}) => {
if (!copyable) {
return null;
}
return (
<CodeHeaderWrapper
className={classNames({
dark: theme === 'dark',
light: theme === 'light'
})}
>
<span>{lang}</span>
<CopyButton
text={copyValue}
size="small"
style={{ color: '#abb2bf' }}
></CopyButton>
</CodeHeaderWrapper>
);
};
const CodeViewer: React.FC<CodeViewerProps> = (props) => {
const {
code,
code = '',
copyValue,
lang,
autodetect = true,
ignoreIllegals = true,
copyable = true,
height = 'auto'
height = 'auto',
style
} = props || {};
const highlightedCode = useMemo(() => {
@@ -59,37 +119,39 @@ const CodeViewer: React.FC<CodeViewerProps> = (props) => {
}, [code, lang, autodetect, ignoreIllegals]);
return (
<pre
className={classNames(
'code-pre custome-scrollbar custom-scrollbar-horizontal ',
{
dark: props.theme === 'dark',
light: props.theme === 'light',
copyable: copyable
}
)}
style={{
height: height
}}
>
<code
style={{ minHeight: height }}
className={classNames(highlightedCode.className, {
dark: props.theme === 'dark',
light: props.theme === 'light'
})}
dangerouslySetInnerHTML={{
__html: highlightedCode.value
<Wrapper>
<CodeHeader
copyValue={copyValue || code}
lang={lang}
copyable={copyable}
theme={props.theme || 'light'}
></CodeHeader>
<pre
className={classNames(
'code-pre custome-scrollbar custom-scrollbar-horizontal ',
{
dark: props.theme === 'dark',
light: props.theme === 'light'
}
)}
style={{
marginBottom: 0,
height: height,
...style
}}
></code>
{copyable && (
<CopyButton
text={code}
size="small"
style={{ color: '#abb2bf' }}
></CopyButton>
)}
</pre>
>
<code
style={{ minHeight: height }}
className={classNames(highlightedCode.className, {
dark: props.theme === 'dark',
light: props.theme === 'light'
})}
dangerouslySetInnerHTML={{
__html: highlightedCode.value
}}
></code>
</pre>
</Wrapper>
);
};
+19 -2
View File
@@ -1,3 +1,4 @@
import useUserSettings from '@/hooks/use-user-settings';
import React from 'react';
import CodeViewerDark from './code-viewer-dark';
import CodeViewerLight from './code-viewer-light';
@@ -8,29 +9,45 @@ const HighlightCode: React.FC<{
lang?: string;
copyable?: boolean;
theme?: 'light' | 'dark';
fixedTheme?: 'light' | 'dark';
height?: string | number;
style?: React.CSSProperties;
copyValue?: string;
}> = (props) => {
const {
style,
code,
copyValue,
lang = 'bash',
copyable = true,
theme = 'dark',
theme,
height = 'auto'
} = props;
const { userSettings } = useUserSettings();
const currentTheme = React.useMemo(() => {
const res = theme || userSettings.theme === 'realDark' ? 'dark' : 'light';
return res;
}, [theme, userSettings.theme]);
return (
<div className="high-light-wrapper hj-wrapper">
{theme === 'dark' ? (
{currentTheme === 'dark' ? (
<CodeViewerDark
lang={lang}
code={code}
copyValue={copyValue}
copyable={copyable}
height={height}
style={style}
/>
) : (
<CodeViewerLight
style={style}
lang={lang}
code={code}
copyValue={copyValue}
copyable={copyable}
height={height}
/>
@@ -31,7 +31,7 @@ hue-6-2: #e6c07b
.code-pre.dark {
.hljs {
color: #abb2bf;
background: #282c34;
background: var(--color-editor-dark);
}
.hljs-comment,
@@ -1,6 +1,6 @@
.high-light-wrapper {
text-align: left;
font-size: var(--font-size-base);
font-size: var(--font-size-code);
.hljs {
font-weight: var(--font-weight-normal);
@@ -41,8 +41,9 @@
.code-pre {
padding-inline: 12px 12px;
border-radius: 0 0 var(--border-radius-mini) var(--border-radius-mini);
position: relative;
border-radius: var(--border-radius-mini);
white-space: pre-wrap;
code {
line-height: 1.5;
@@ -59,11 +60,25 @@
}
&.dark {
background-color: #282c34;
background-color: var(--color-editor-dark);
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-handle-light-bg);
border-radius: 4px;
}
}
}
&.light {
background-color: rgb(250, 250, 250);
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb);
border-radius: 4px;
}
}
}
}
}
@@ -31,7 +31,7 @@ hue-6-2: #c18401
.code-pre.light {
.hljs {
color: #383a42;
background: #fafafa;
background: var(--color-editor-light);
}
.hljs-comment,
@@ -0,0 +1,351 @@
@font-face {
font-family: iconfont; /* Project id 4613488 */
src: url('iconfont.woff2?t=1747231573342') format('woff2'),
url('iconfont.woff?t=1747231573342') format('woff'),
url('iconfont.ttf?t=1747231573342') format('truetype');
}
.iconfont {
font-family: iconfont !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-dark_theme::before {
content: "\e646";
}
.icon-theme-auto-1::before {
content: "\e660";
}
.icon-theme-auto::before {
content: "\e663";
}
.icon-auto-theme-1::before {
content: "\e664";
}
.icon-auto-theme::before {
content: "\e662";
}
.icon-cols_3::before {
content: "\e65c";
}
.icon-cols_6::before {
content: "\e65d";
}
.icon-cols_2::before {
content: "\e65e";
}
.icon-cols_4::before {
content: "\e65f";
}
.icon-a-save1::before {
content: "\e65b";
}
.icon-uncollapse_all::before {
content: "\e657";
}
.icon-collapse::before {
content: "\e656";
}
.icon-rocket-launch::before {
content: "\e689";
}
.icon-user-filled::before {
content: "\e625";
}
.icon-assistant::before {
content: "\e62d";
}
.icon-assistant-filled::before {
content: "\e847";
}
.icon-save3::before {
content: "\e655";
}
.icon-fankuifaqs::before {
content: "\e7bf";
}
.icon-issues::before {
content: "\e816";
}
.icon-neicun::before {
content: "\e688";
}
.icon-collapse_all::before {
content: "\e66f";
}
.icon-down::before {
content: "\e654";
}
.icon-fenxiang::before {
content: "\e604";
}
.icon-mosaic-2::before {
content: "\e64f";
}
.icon-stars::before {
content: "\e8a8";
}
.icon-mosaic::before {
content: "\e636";
}
.icon-outline-play::before {
content: "\e653";
}
.icon-SelectionInverse::before {
content: "\eace";
}
.icon-justice1::before {
content: "\e652";
}
.icon-New_img::before {
content: "\e733";
}
.icon-new_release_outlined::before {
content: "\e66c";
}
.icon-new-releases::before {
content: "\e60f";
}
.icon-catalog::before {
content: "\e62b";
}
.icon-save2::before {
content: "\e635";
}
.icon-left-template::before {
content: "\e62a";
}
.icon-logs::before {
content: "\e6ec";
}
.icon-gpu::before {
content: "\e71e";
}
.icon-filled-gpu::before {
content: "\e6de";
}
.icon-outline-gpu::before {
content: "\e641";
}
.icon-ts-tubiao_webserver::before {
content: "\e716";
}
.icon-server::before {
content: "\e66a";
}
.icon-host::before {
content: "\e7c6";
}
.icon-playcircle::before {
content: "\e80f";
}
.icon-recreate::before {
content: "\e6a5";
}
.icon-save1::before {
content: "\e647";
}
.icon-save::before {
content: "\e67c";
}
.icon-upload_image::before {
content: "\e613";
}
.icon-sound-wave::before {
content: "\e619";
}
.icon-rank1::before {
content: "\e7cb";
}
.icon-cube::before {
content: "\e769";
}
.icon-speaker-slash::before {
content: "\ebb6";
}
.icon-random::before {
content: "\e603";
}
.icon-suijisenlin::before {
content: "\e60e";
}
.icon-stop2::before {
content: "\e8db";
}
.icon-stop::before {
content: "\e60b";
}
.icon-image::before {
content: "\e62c";
}
.icon-stopcircle-fill::before {
content: "\e7cc";
}
.icon-playcircle-fill::before {
content: "\e7cd";
}
.icon-SpeakerSlash::before {
content: "\e661";
}
.icon-SpeakerHigh::before {
content: "\e670";
}
.icon-user_voice::before {
content: "\e667";
}
.icon-audio::before {
content: "\e985";
}
.icon-hard-disk::before {
content: "\eb1d";
}
.icon-new::before {
content: "\e612";
}
.icon-tu2::before {
content: "\e607";
}
.icon-robot::before {
content: "\e634";
}
.icon-robot1::before {
content: "\e602";
}
.icon-aizhineng::before {
content: "\e672";
}
.icon-copy::before {
content: "\e720";
}
.icon-AIzhineng::before {
content: "\e608";
}
.icon-clear::before {
content: "\e60a";
}
.icon-keyboard::before {
content: "\e61b";
}
.icon-networkerror::before {
content: "\e624";
}
.icon-external-link::before {
content: "\e66b";
}
.icon-huggingface::before {
content: "\e7d1";
}
.icon-ollama::before {
content: "\e601";
}
.icon-a-layout6-line::before {
content: "\e9ef";
}
.icon-a-Layout5::before {
content: "\e610";
}
.icon-English::before {
content: "\e8b3";
}
.icon-chinese::before {
content: "\e611";
}
.icon-yingguo::before {
content: "\e606";
}
.icon-code::before {
content: "\e84f";
}
.icon-stop1::before {
content: "\e783";
}
.icon-command::before {
content: "\e600";
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,597 @@
{
"id": "4613488",
"name": "gpustack",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "44028047",
"name": "dark_theme",
"font_class": "dark_theme",
"unicode": "e646",
"unicode_decimal": 58950
},
{
"icon_id": "44094552",
"name": "theme-auto-1",
"font_class": "theme-auto-1",
"unicode": "e660",
"unicode_decimal": 58976
},
{
"icon_id": "44094551",
"name": "theme-auto",
"font_class": "theme-auto",
"unicode": "e663",
"unicode_decimal": 58979
},
{
"icon_id": "44094550",
"name": "theme",
"font_class": "auto-theme-1",
"unicode": "e664",
"unicode_decimal": 58980
},
{
"icon_id": "44092227",
"name": "routine_36dp_1F1F1F_FILL0_wght400_GRAD0_opsz24",
"font_class": "auto-theme",
"unicode": "e662",
"unicode_decimal": 58978
},
{
"icon_id": "44086334",
"name": "cols_3",
"font_class": "cols_3",
"unicode": "e65c",
"unicode_decimal": 58972
},
{
"icon_id": "44086333",
"name": "cols_6",
"font_class": "cols_6",
"unicode": "e65d",
"unicode_decimal": 58973
},
{
"icon_id": "44086332",
"name": "cols_2",
"font_class": "cols_2",
"unicode": "e65e",
"unicode_decimal": 58974
},
{
"icon_id": "44086331",
"name": "cols_4",
"font_class": "cols_4",
"unicode": "e65f",
"unicode_decimal": 58975
},
{
"icon_id": "44065884",
"name": "save",
"font_class": "a-save1",
"unicode": "e65b",
"unicode_decimal": 58971
},
{
"icon_id": "43808249",
"name": "uncollapse_all",
"font_class": "uncollapse_all",
"unicode": "e657",
"unicode_decimal": 58967
},
{
"icon_id": "43807590",
"name": "collapse",
"font_class": "collapse",
"unicode": "e656",
"unicode_decimal": 58966
},
{
"icon_id": "22575690",
"name": "rocket-launch",
"font_class": "rocket-launch",
"unicode": "e689",
"unicode_decimal": 59017
},
{
"icon_id": "6050653",
"name": "user",
"font_class": "user-filled",
"unicode": "e625",
"unicode_decimal": 58917
},
{
"icon_id": "37486588",
"name": "assistant",
"font_class": "assistant",
"unicode": "e62d",
"unicode_decimal": 58925
},
{
"icon_id": "43220044",
"name": "AIAssistant",
"font_class": "assistant-filled",
"unicode": "e847",
"unicode_decimal": 59463
},
{
"icon_id": "43626126",
"name": "save",
"font_class": "save3",
"unicode": "e655",
"unicode_decimal": 58965
},
{
"icon_id": "20186129",
"name": "反馈faqs",
"font_class": "fankuifaqs",
"unicode": "e7bf",
"unicode_decimal": 59327
},
{
"icon_id": "42800361",
"name": "issues",
"font_class": "issues",
"unicode": "e816",
"unicode_decimal": 59414
},
{
"icon_id": "8211955",
"name": "ram",
"font_class": "neicun",
"unicode": "e688",
"unicode_decimal": 59016
},
{
"icon_id": "37125227",
"name": "collapse_all",
"font_class": "collapse_all",
"unicode": "e66f",
"unicode_decimal": 58991
},
{
"icon_id": "43363249",
"name": "down",
"font_class": "down",
"unicode": "e654",
"unicode_decimal": 58964
},
{
"icon_id": "159932",
"name": "search",
"font_class": "fenxiang",
"unicode": "e604",
"unicode_decimal": 58884
},
{
"icon_id": "40678285",
"name": "mosaic",
"font_class": "mosaic-2",
"unicode": "e64f",
"unicode_decimal": 58959
},
{
"icon_id": "22859935",
"name": "stars",
"font_class": "stars",
"unicode": "e8a8",
"unicode_decimal": 59560
},
{
"icon_id": "39566959",
"name": "mosaic",
"font_class": "mosaic",
"unicode": "e636",
"unicode_decimal": 58934
},
{
"icon_id": "43256197",
"name": "play circle",
"font_class": "outline-play",
"unicode": "e653",
"unicode_decimal": 58963
},
{
"icon_id": "27116743",
"name": "SelectionInverse",
"font_class": "SelectionInverse",
"unicode": "eace",
"unicode_decimal": 60110
},
{
"icon_id": "43035036",
"name": "justice",
"font_class": "justice1",
"unicode": "e652",
"unicode_decimal": 58962
},
{
"icon_id": "1740853",
"name": "New_img",
"font_class": "New_img",
"unicode": "e733",
"unicode_decimal": 59187
},
{
"icon_id": "9745732",
"name": "new_release_outlined",
"font_class": "new_release_outlined",
"unicode": "e66c",
"unicode_decimal": 58988
},
{
"icon_id": "40667024",
"name": "new-releases",
"font_class": "new-releases",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "41920256",
"name": "catalog",
"font_class": "catalog",
"unicode": "e62b",
"unicode_decimal": 58923
},
{
"icon_id": "13851416",
"name": "save",
"font_class": "save2",
"unicode": "e635",
"unicode_decimal": 58933
},
{
"icon_id": "3238366",
"name": "模板",
"font_class": "left-template",
"unicode": "e62a",
"unicode_decimal": 58922
},
{
"icon_id": "26746089",
"name": "logs",
"font_class": "logs",
"unicode": "e6ec",
"unicode_decimal": 59116
},
{
"icon_id": "1132892",
"name": "gpu",
"font_class": "gpu",
"unicode": "e71e",
"unicode_decimal": 59166
},
{
"icon_id": "38418656",
"name": "GPUxCPU",
"font_class": "filled-gpu",
"unicode": "e6de",
"unicode_decimal": 59102
},
{
"icon_id": "7556336",
"name": "gpu",
"font_class": "outline-gpu",
"unicode": "e641",
"unicode_decimal": 58945
},
{
"icon_id": "8040181",
"name": "webserver",
"font_class": "ts-tubiao_webserver",
"unicode": "e716",
"unicode_decimal": 59158
},
{
"icon_id": "6587064",
"name": "server",
"font_class": "server",
"unicode": "e66a",
"unicode_decimal": 58986
},
{
"icon_id": "40493325",
"name": "host",
"font_class": "host",
"unicode": "e7c6",
"unicode_decimal": 59334
},
{
"icon_id": "6151262",
"name": "play circle",
"font_class": "playcircle",
"unicode": "e80f",
"unicode_decimal": 59407
},
{
"icon_id": "25197526",
"name": "recreate",
"font_class": "recreate",
"unicode": "e6a5",
"unicode_decimal": 59045
},
{
"icon_id": "12689724",
"name": "save",
"font_class": "save1",
"unicode": "e647",
"unicode_decimal": 58951
},
{
"icon_id": "15838524",
"name": "save",
"font_class": "save",
"unicode": "e67c",
"unicode_decimal": 59004
},
{
"icon_id": "8782480",
"name": "upload_image",
"font_class": "upload_image",
"unicode": "e613",
"unicode_decimal": 58899
},
{
"icon_id": "14464883",
"name": "sound-wave",
"font_class": "sound-wave",
"unicode": "e619",
"unicode_decimal": 58905
},
{
"icon_id": "37200543",
"name": "rank",
"font_class": "rank1",
"unicode": "e7cb",
"unicode_decimal": 59339
},
{
"icon_id": "5127275",
"name": "cube",
"font_class": "cube",
"unicode": "e769",
"unicode_decimal": 59241
},
{
"icon_id": "28326047",
"name": "speaker-slash",
"font_class": "speaker-slash",
"unicode": "ebb6",
"unicode_decimal": 60342
},
{
"icon_id": "13059398",
"name": "随机",
"font_class": "random",
"unicode": "e603",
"unicode_decimal": 58883
},
{
"icon_id": "5978743",
"name": "random-forest",
"font_class": "suijisenlin",
"unicode": "e60e",
"unicode_decimal": 58894
},
{
"icon_id": "15617444",
"name": "stop",
"font_class": "stop2",
"unicode": "e8db",
"unicode_decimal": 59611
},
{
"icon_id": "8327181",
"name": "stop",
"font_class": "stop",
"unicode": "e60b",
"unicode_decimal": 58891
},
{
"icon_id": "714031",
"name": "image",
"font_class": "image",
"unicode": "e62c",
"unicode_decimal": 58924
},
{
"icon_id": "6151140",
"name": "stop circle-fill",
"font_class": "stopcircle-fill",
"unicode": "e7cc",
"unicode_decimal": 59340
},
{
"icon_id": "6151141",
"name": "play circle-fill",
"font_class": "playcircle-fill",
"unicode": "e7cd",
"unicode_decimal": 59341
},
{
"icon_id": "23563264",
"name": "SpeakerSlash",
"font_class": "SpeakerSlash",
"unicode": "e661",
"unicode_decimal": 58977
},
{
"icon_id": "23563318",
"name": "SpeakerHigh",
"font_class": "SpeakerHigh",
"unicode": "e670",
"unicode_decimal": 58992
},
{
"icon_id": "41955357",
"name": " user_voice",
"font_class": "user_voice",
"unicode": "e667",
"unicode_decimal": 58983
},
{
"icon_id": "29174196",
"name": "audio",
"font_class": "audio",
"unicode": "e985",
"unicode_decimal": 59781
},
{
"icon_id": "40073465",
"name": "hard-disk",
"font_class": "hard-disk",
"unicode": "eb1d",
"unicode_decimal": 60189
},
{
"icon_id": "12717507",
"name": "new",
"font_class": "new",
"unicode": "e612",
"unicode_decimal": 58898
},
{
"icon_id": "36135335",
"name": "mdp-template-modelscope",
"font_class": "tu2",
"unicode": "e607",
"unicode_decimal": 58887
},
{
"icon_id": "10710692",
"name": "robot",
"font_class": "robot",
"unicode": "e634",
"unicode_decimal": 58932
},
{
"icon_id": "12070718",
"name": "robot",
"font_class": "robot1",
"unicode": "e602",
"unicode_decimal": 58882
},
{
"icon_id": "10791813",
"name": "ai智能",
"font_class": "aizhineng",
"unicode": "e672",
"unicode_decimal": 58994
},
{
"icon_id": "19418384",
"name": "copy",
"font_class": "copy",
"unicode": "e720",
"unicode_decimal": 59168
},
{
"icon_id": "40757798",
"name": "AI智能",
"font_class": "AIzhineng",
"unicode": "e608",
"unicode_decimal": 58888
},
{
"icon_id": "36263088",
"name": "clear",
"font_class": "clear",
"unicode": "e60a",
"unicode_decimal": 58890
},
{
"icon_id": "5297059",
"name": "keyboard",
"font_class": "keyboard",
"unicode": "e61b",
"unicode_decimal": 58907
},
{
"icon_id": "11539157",
"name": "network error",
"font_class": "networkerror",
"unicode": "e624",
"unicode_decimal": 58916
},
{
"icon_id": "14465073",
"name": "external-link",
"font_class": "external-link",
"unicode": "e66b",
"unicode_decimal": 58987
},
{
"icon_id": "38465464",
"name": "huggingface",
"font_class": "huggingface",
"unicode": "e7d1",
"unicode_decimal": 59345
},
{
"icon_id": "40568411",
"name": "ollama",
"font_class": "ollama",
"unicode": "e601",
"unicode_decimal": 58881
},
{
"icon_id": "24341846",
"name": "layout 6-line",
"font_class": "a-layout6-line",
"unicode": "e9ef",
"unicode_decimal": 59887
},
{
"icon_id": "31345353",
"name": "a-Layout5",
"font_class": "a-Layout5",
"unicode": "e610",
"unicode_decimal": 58896
},
{
"icon_id": "10031665",
"name": "English",
"font_class": "English",
"unicode": "e8b3",
"unicode_decimal": 59571
},
{
"icon_id": "9134244",
"name": "chinese",
"font_class": "chinese",
"unicode": "e611",
"unicode_decimal": 58897
},
{
"icon_id": "12176725",
"name": "english",
"font_class": "yingguo",
"unicode": "e606",
"unicode_decimal": 58886
},
{
"icon_id": "34453176",
"name": "code",
"font_class": "code",
"unicode": "e84f",
"unicode_decimal": 59471
},
{
"icon_id": "12579515",
"name": "stop",
"font_class": "stop1",
"unicode": "e783",
"unicode_decimal": 59267
},
{
"icon_id": "1842082",
"name": "command",
"font_class": "command",
"unicode": "e600",
"unicode_decimal": 58880
}
]
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -1
View File
@@ -1,7 +1,8 @@
import { createFromIconfontCN } from '@ant-design/icons';
import './iconfont/iconfont.js';
const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_4613488_l9554igzbh.js'
scriptUrl: ''
});
export default IconFont;
@@ -0,0 +1,149 @@
/**
* Creates a canvas element and its rendering context.
* @param {number} width - Canvas width.
* @param {number} height - Canvas height.
* @returns {{ canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D }} - The created canvas and context.
*/
function createCanvas(
width: number,
height: number
): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return { canvas, ctx: canvas.getContext('2d')! };
}
/**
* Checks if a pixel is white.
* @param {Uint8ClampedArray} pixels - The image pixel data.
* @param {number} x - The pixel's X coordinate.
* @param {number} y - The pixel's Y coordinate.
* @param {number} width - The image width.
* @returns {boolean} - Whether the pixel is white.
*/
function isWhite(
pixels: Uint8ClampedArray,
x: number,
y: number,
width: number
): boolean {
let index = (y * width + x) * 4;
return (
pixels[index] === 255 &&
pixels[index + 1] === 255 &&
pixels[index + 2] === 255
);
}
/**
* Performs a flood fill to find all connected white pixels.
* @param {Uint8ClampedArray} pixels - The image pixel data.
* @param {number} width - The image width.
* @param {number} height - The image height.
* @param {number} x - The starting X coordinate.
* @param {number} y - The starting Y coordinate.
* @param {boolean[][]} visited - A 2D array to track visited pixels.
* @param {Array<{ x: number, y: number }>} block - The list of coordinates forming a white block.
*/
function floodFill(
pixels: Uint8ClampedArray,
width: number,
height: number,
x: number,
y: number,
visited: boolean[][],
block: Array<{ x: number; y: number }>
): void {
let stack: Array<[number, number]> = [[x, y]];
let directions: Array<[number, number]> = [
[1, 0],
[-1, 0],
[0, 1],
[0, -1], // 4-directional search (can be extended to 8-directional)
[1, 1],
[-1, -1],
[1, -1],
[-1, 1] // Diagonal directions
];
while (stack.length) {
let [cx, cy] = stack.pop()!;
if (
cx < 0 ||
cy < 0 ||
cx >= width ||
cy >= height ||
visited[cy][cx] ||
!isWhite(pixels, cx, cy, width)
) {
continue;
}
visited[cy][cx] = true;
block.push({ x: cx, y: cy });
directions.forEach(([dx, dy]) => stack.push([cx + dx, cy + dy]));
}
}
/**
* Extracts all white blocks from an image.
* @param {ImageData} imageData - The image pixel data.
* @returns {Array<Array<{ x: number, y: number }>>} - List of white blocks, each containing pixel coordinates.
*/
function getWhiteBlocks(
imageData: ImageData
): Array<Array<{ x: number; y: number }>> {
const { data, width, height } = imageData;
let visited: boolean[][] = Array.from({ length: height }, () =>
new Array(width).fill(false)
);
let whiteBlocks: Array<Array<{ x: number; y: number }>> = [];
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (isWhite(data, x, y, width) && !visited[y][x]) {
let block: Array<{ x: number; y: number }> = [];
floodFill(data, width, height, x, y, visited, block);
whiteBlocks.push(block);
}
}
}
return whiteBlocks;
}
/**
* Loads an image from a file.
* @param {File} file - The image file.
* @returns {Promise<HTMLImageElement>} - The loaded image element.
*/
function loadImage(file: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = file;
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Image loading failed'));
});
}
/**
* Processes the image file and extracts white blocks.
* @param {File} file - The uploaded image file.
* @returns {Promise<Array<Array<{ x: number, y: number }>>>} - List of white blocks with pixel coordinates.
*/
async function processImage(
file: string
): Promise<Array<Array<{ x: number; y: number }>>> {
const img = await loadImage(file);
const { canvas, ctx } = createCanvas(img.width, img.height);
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
const whiteBlocks = getWhiteBlocks(imageData);
URL.revokeObjectURL(img.src);
return whiteBlocks;
}
export { processImage };
@@ -0,0 +1,437 @@
import _ from 'lodash';
import React, { useCallback, useMemo, useRef } from 'react';
type Point = { x: number; y: number; lineWidth: number };
type Stroke = Point[];
const COLOR = 'rgba(0, 0, 255, 0.3)';
export default function useDrawing(props: {
isDisabled?: boolean;
invertMask: boolean;
maskUpload?: any[];
lineWidth: number;
translatePos: { current: { x: number; y: number } };
onSave: (imageData: { mask: string | null; img: string }) => void;
}) {
const {
isDisabled,
invertMask,
maskUpload,
lineWidth,
translatePos,
onSave
} = props;
const mouseDownState = useRef<boolean>(false);
const canvasRef = useRef<HTMLCanvasElement>(null);
const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
const offscreenCanvasRef = useRef<any>(null);
const currentStroke = useRef<Point[]>([]);
const strokesRef = useRef<Stroke[]>([]);
const isDrawing = useRef<boolean>(false);
const cursorRef = useRef<HTMLDivElement>(null);
const autoScale = useRef<number>(1);
const baseScale = useRef<number>(1);
const contentPos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const maskStorkeRef = useRef<Stroke[]>([]);
const isLoadingMaskRef = useRef<boolean>(false);
const disabled = useMemo(() => {
return isDisabled || invertMask || !!maskUpload?.length;
}, [isDisabled, invertMask, maskUpload]);
const setStrokes = (strokes: Stroke[]) => {
strokesRef.current = strokes;
};
const setMaskStrokes = (strokes: Stroke[]) => {
maskStorkeRef.current = strokes;
};
const inpaintArea = useCallback(
(data: Uint8ClampedArray<ArrayBufferLike>) => {
for (let i = 0; i < data.length; i += 4) {
const alpha = data[i + 3];
if (alpha > 0) {
data[i] = 255; // Red
data[i + 1] = 255; // Green
data[i + 2] = 255; // Blue
data[i + 3] = 255; // Alpha
}
}
},
[]
);
const generateImage = useCallback(() => {
const canvas = canvasRef.current!;
return canvas.toDataURL('image/png');
}, []);
const generateMask = useCallback(() => {
if (strokesRef.current.length === 0 && maskStorkeRef.current.length === 0) {
return null;
}
const overlayCanvas = overlayCanvasRef.current!;
const maskCanvas = document.createElement('canvas');
maskCanvas.width = overlayCanvas.width;
maskCanvas.height = overlayCanvas.height;
const maskCtx = maskCanvas.getContext('2d')!;
const overlayCtx = overlayCanvas.getContext('2d')!;
const imageData = overlayCtx.getImageData(
0,
0,
overlayCanvas.width,
overlayCanvas.height
);
const data = imageData.data;
inpaintArea(data);
maskCtx.putImageData(imageData, 0, 0);
maskCtx.globalCompositeOperation = 'destination-over';
maskCtx.fillStyle = 'black';
maskCtx.fillRect(0, 0, maskCanvas.width, maskCanvas.height);
return maskCanvas.toDataURL('image/png');
}, []);
const saveImage = () => {
const mask = generateMask();
const img = generateImage();
onSave({ mask, img });
};
const creatOffscreenCanvas = useCallback(() => {
if (!offscreenCanvasRef.current) {
offscreenCanvasRef.current = document.createElement('canvas');
}
}, []);
const setTransform = useCallback(() => {
creatOffscreenCanvas();
const ctx = canvasRef.current?.getContext('2d');
const overlayCtx = overlayCanvasRef.current?.getContext('2d');
const offCtx = offscreenCanvasRef.current?.getContext('2d');
if (!ctx || !overlayCtx) return;
ctx!.resetTransform();
overlayCtx!.resetTransform();
offCtx!.resetTransform();
const { current: scale } = autoScale;
const { x: translateX, y: translateY } = translatePos.current;
ctx!.setTransform(scale, 0, 0, scale, translateX, translateY);
overlayCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
offCtx!.setTransform(scale, 0, 0, scale, translateX, translateY);
}, []);
const getTransformedPoint = (offsetX: number, offsetY: number) => {
const { current: scale } = autoScale;
console.log('lineWidth:----------', lineWidth, autoScale.current);
const { x: translateX, y: translateY } = translatePos.current;
const transformedX = (offsetX - translateX) / scale;
const transformedY = (offsetY - translateY) / scale;
return {
x: transformedX,
y: transformedY
};
};
const getTransformLineWidth = (w = 1) => {
console.log('lineWidth:', lineWidth, autoScale.current);
const width = w || lineWidth;
return width / autoScale.current;
};
const drawLine = useCallback(
(
ctx: CanvasRenderingContext2D,
point: Point,
options: {
lineWidth: number;
color: string;
compositeOperation: 'source-over' | 'destination-out';
}
) => {
const { lineWidth, color, compositeOperation } = options;
ctx.lineWidth = getTransformLineWidth(lineWidth);
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalCompositeOperation = compositeOperation;
const { x, y } = getTransformedPoint(point.x, point.y);
ctx.lineTo(x, y);
if (compositeOperation === 'source-over') {
ctx.strokeStyle = color;
}
ctx.stroke();
},
[getTransformLineWidth]
);
const drawStroke = useCallback(
(
ctx: CanvasRenderingContext2D,
stroke: Stroke | Point[],
options: {
lineWidth?: number;
color: string;
compositeOperation: 'source-over' | 'destination-out';
}
) => {
const { color, compositeOperation } = options;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalCompositeOperation = compositeOperation;
ctx.save();
ctx.beginPath();
stroke.forEach((point, i) => {
const { x, y } = getTransformedPoint(point.x, point.y);
ctx.lineWidth = getTransformLineWidth(point.lineWidth);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
if (compositeOperation === 'source-over') {
ctx.strokeStyle = color;
}
ctx.stroke();
ctx.restore();
},
[getTransformLineWidth, getTransformedPoint]
);
const draw = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
console.log(
'Drawing:',
isDrawing.current,
currentStroke.current,
strokesRef.current
);
if (!isDrawing.current || !mouseDownState.current) return;
const { offsetX, offsetY } = e.nativeEvent;
const currentX = offsetX;
const currentY = offsetY;
console.log('currentStroke:', currentStroke.current);
currentStroke.current.push({
x: currentX,
y: currentY,
lineWidth
});
const ctx = overlayCanvasRef.current!.getContext('2d');
ctx!.save();
drawLine(
ctx!,
{ x: currentX, y: currentY, lineWidth },
{ lineWidth, color: COLOR, compositeOperation: 'destination-out' }
);
drawLine(
ctx!,
{ x: currentX, y: currentY, lineWidth },
{ lineWidth, color: COLOR, compositeOperation: 'source-over' }
);
ctx!.restore();
};
const startDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
isDrawing.current = true;
currentStroke.current = [];
const { offsetX, offsetY } = e.nativeEvent;
const currentX = offsetX;
const currentY = offsetY;
currentStroke.current.push({
x: currentX,
y: currentY,
lineWidth
});
const ctx = overlayCanvasRef.current!.getContext('2d');
setTransform();
const { x, y } = getTransformedPoint(currentX, currentY);
ctx!.beginPath();
ctx!.moveTo(x, y);
draw(e);
};
const endDrawing = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
if (!isDrawing.current) {
return;
}
console.log('End Drawing:', e);
isDrawing.current = false;
strokesRef.current.push(_.cloneDeep(currentStroke.current));
currentStroke.current = [];
saveImage();
};
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (disabled) {
return;
}
overlayCanvasRef.current!.style.cursor = 'none';
cursorRef.current!.style.display = 'block';
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
};
const handleMouseLeave = () => {
if (disabled) {
return;
}
isDrawing.current = false;
overlayCanvasRef.current!.style.cursor = 'default';
cursorRef.current!.style.display = 'none';
};
const handleMouseEnter = (e: React.MouseEvent<HTMLCanvasElement>) => {
console.log('mouse enter:', mouseDownState.current);
if (disabled) {
overlayCanvasRef.current!.style.cursor = 'default';
return;
}
// if (mouseDownState.current) {
// isDrawing.current = true;
// }
overlayCanvasRef.current!.style.cursor = 'none';
cursorRef.current!.style.display = 'block';
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
};
const clearOverlayCanvas = useCallback(() => {
const ctx = overlayCanvasRef.current!.getContext('2d');
const offCtx = offscreenCanvasRef.current?.getContext('2d');
offCtx!.resetTransform();
ctx!.resetTransform();
ctx!.clearRect(
0,
0,
overlayCanvasRef.current!.width,
overlayCanvasRef.current!.height
);
offCtx!.clearRect(
0,
0,
overlayCanvasRef.current!.width,
overlayCanvasRef.current!.height
);
}, []);
const clearCanvas = useCallback(() => {
const canvas = canvasRef.current!;
const ctx = canvasRef.current!.getContext('2d');
const offCtx = offscreenCanvasRef.current?.getContext('2d');
ctx!.resetTransform();
ctx!.clearRect(0, 0, canvas.width, canvas.height);
offCtx!.resetTransform();
offCtx!.clearRect(0, 0, canvas.width, canvas.height);
}, []);
const resetCanvas = useCallback(() => {
const canvas = canvasRef.current!;
const overlayCanvas = overlayCanvasRef.current!;
const offscreenCanvas = offscreenCanvasRef.current!;
const ctx = canvas.getContext('2d');
const overlayCtx = overlayCanvas.getContext('2d');
const offCtx = offscreenCanvasRef.current?.getContext('2d');
autoScale.current = 1;
baseScale.current = 1;
translatePos.current = { x: 0, y: 0 };
contentPos.current = { x: 0, y: 0 };
canvas.style.transform = 'scale(1)';
overlayCanvas.style.transform = 'scale(1)';
offscreenCanvas.style.transform = 'scale(1)';
cursorRef.current!.style.width = `${lineWidth}px`;
cursorRef.current!.style.height = `${lineWidth}px`;
ctx!.resetTransform();
overlayCtx!.resetTransform();
offCtx!.resetTransform();
}, []);
const fitView = () => {
resetCanvas();
autoScale.current = baseScale.current;
translatePos.current = { x: 0, y: 0 };
setTransform();
overlayCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
canvasRef.current!.style.transform = `scale(${autoScale.current})`;
offscreenCanvasRef.current!.style.transform = `scale(${autoScale.current})`;
};
return {
canvasRef,
overlayCanvasRef,
offscreenCanvasRef,
cursorRef,
strokesRef,
currentStroke,
isDrawing,
mouseDownState,
autoScale,
baseScale,
maskStorkeRef,
isLoadingMaskRef,
creatOffscreenCanvas,
setMaskStrokes,
fitView,
setStrokes,
resetCanvas,
draw,
getTransformLineWidth,
getTransformedPoint,
drawStroke,
startDrawing,
endDrawing,
handleMouseMove,
handleMouseLeave,
handleMouseEnter,
saveImage,
generateMask,
generateImage,
setTransform,
clearOverlayCanvas,
clearCanvas
};
}
@@ -0,0 +1,129 @@
import _ from 'lodash';
import React, { MutableRefObject, useState } from 'react';
export default function useZoom(props: {
overlayCanvasRef: any;
canvasRef: any;
offscreenCanvasRef: any;
cursorRef: any;
lineWidth: number;
autoScale: MutableRefObject<number>;
baseScale: MutableRefObject<number>;
translatePos: MutableRefObject<{ x: number; y: number }>;
isLoadingMaskRef: MutableRefObject<boolean>;
}) {
const MIN_SCALE = 0.2;
const MAX_SCALE = 8;
const ZOOM_SPEED = 0.1;
const {
overlayCanvasRef,
canvasRef,
offscreenCanvasRef,
cursorRef,
lineWidth,
translatePos,
autoScale,
baseScale,
isLoadingMaskRef
} = props;
const [activeScale, setActiveScale] = useState<number>(1);
const setCanvasTransformOrigin = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (autoScale.current <= MIN_SCALE) {
return;
}
if (autoScale.current >= MAX_SCALE) {
return;
}
const rect = overlayCanvasRef.current!.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const originX = mouseX / rect.width;
const originY = mouseY / rect.height;
overlayCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
canvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
offscreenCanvasRef.current!.style.transformOrigin = `${originX * 100}% ${originY * 100}%`;
};
const updateZoom = (scaleChange: number, mouseX: number, mouseY: number) => {
const newScale = _.round(autoScale.current + scaleChange, 2);
if (newScale < MIN_SCALE || newScale > MAX_SCALE) return;
const { current: oldScale } = autoScale;
const { x: oldTranslateX, y: oldTranslateY } = translatePos.current;
const centerX = (mouseX - oldTranslateX) / oldScale;
const centerY = (mouseY - oldTranslateY) / oldScale;
autoScale.current = newScale;
const newTranslateX = mouseX - centerX * newScale;
const newTranslateY = mouseY - centerY * newScale;
translatePos.current = { x: newTranslateX, y: newTranslateY };
};
const applyCanvasTransform = () => {
const scale = autoScale.current;
const transform = `scale(${scale})`;
overlayCanvasRef.current!.style.transform = transform;
canvasRef.current!.style.transform = transform;
offscreenCanvasRef.current!.style.transform = transform;
};
const handleZoom = (event: React.WheelEvent<HTMLCanvasElement>) => {
const scaleChange = event.deltaY > 0 ? -ZOOM_SPEED : ZOOM_SPEED;
// current mouse position
const canvas = overlayCanvasRef.current!;
const rect = canvas.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
applyCanvasTransform();
setCanvasTransformOrigin(event);
updateZoom(scaleChange, mouseX, mouseY);
};
const updateCursorSize = () => {
cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`;
cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`;
};
const updateCursorPosOnZoom = (e: any) => {
cursorRef.current!.style.top = `${e.clientY - (lineWidth / 2) * autoScale.current}px`;
cursorRef.current!.style.left = `${e.clientX - (lineWidth / 2) * autoScale.current}px`;
};
const handleOnWheel = (event: any) => {
if (isLoadingMaskRef.current) {
return;
}
event.preventDefault();
// stop
handleZoom(event);
updateCursorSize();
updateCursorPosOnZoom(event);
setActiveScale(autoScale.current);
};
const throttleHandleOnWheel = _.throttle((event: any) => {
handleOnWheel(event);
}, 16);
return {
handleOnWheel: handleOnWheel,
setActiveScale,
activeScale,
autoScale,
baseScale
};
}
+39
View File
@@ -0,0 +1,39 @@
.editor-wrapper {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
.tools {
margin-bottom: 10px;
display: flex;
gap: 10px;
}
.editor-content {
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.overlay-canvas:hover {
cursor: none !important;
}
.overlay-canvas.overlay-canvas--disabled:hover {
cursor: default !important;
}
.upload-mask {
&:hover {
.close-btn {
display: block;
}
}
}
.close-btn {
display: none;
}
}
+636
View File
@@ -0,0 +1,636 @@
import { Spin } from 'antd';
import classNames from 'classnames';
import dayjs from 'dayjs';
import React, {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState
} from 'react';
import styled from 'styled-components';
import useDrawing from './hooks/use-drawing';
import useZoom from './hooks/use-zoom';
import './index.less';
import { ImageActionsBar, ToolsBar } from './tools-bar';
const LoadWrapper = styled.div<{ width?: number; height?: number }>`
position: absolute;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: ${(props) => `${props.width}px` || '100%'};
z-index: 100;
`;
const Loading = (props: { width: number; height: number }) => {
const { width, height } = props;
return (
<LoadWrapper width={width} height={height}>
<Spin spinning></Spin>
</LoadWrapper>
);
};
type Point = { x: number; y: number; lineWidth: number };
type Stroke = Point[];
type CanvasImageEditorProps = {
ref?: any;
loading: boolean;
imageSrc: string;
disabled?: boolean;
imguid: string | number;
maskUpload?: any[];
clearUploadMask?: () => void;
onSave: (imageData: { mask: string | null; img: string }) => void;
onScaleImageSize?: (data: { width: number; height: number }) => void;
handleUpdateImageList: (fileList: any[]) => void;
handleUpdateMaskList: (fileList: any[]) => void;
uploadButton?: React.ReactNode;
accept?: string;
imageStatus: {
isOriginal: boolean;
isResetNeeded: boolean;
width: number;
height: number;
};
};
const COLOR = 'rgba(0, 0, 255, 0.3)';
const CanvasImageEditor: React.FC<CanvasImageEditorProps> = forwardRef(
(
{
loading,
imageSrc,
disabled: isDisabled,
imageStatus,
maskUpload,
accept,
onSave,
onScaleImageSize,
handleUpdateImageList,
handleUpdateMaskList
},
ref
) => {
const invertWorkerRef = useRef<any>(null);
const loadMaksWorkerRef = useRef<any>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [lineWidth, setLineWidth] = useState<number>(60);
const translatePos = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
const negativeMaskRef = useRef<boolean>(false);
const [invertMask, setInvertMask] = useState<boolean>(false);
const timer = useRef<any>(null);
const [loadingSize, setLoadingSize] = useState({
width: 0,
height: 0,
loading: false
});
const {
canvasRef,
overlayCanvasRef,
offscreenCanvasRef,
cursorRef,
strokesRef,
currentStroke,
mouseDownState,
autoScale,
baseScale,
maskStorkeRef,
isLoadingMaskRef,
setMaskStrokes,
draw,
drawStroke,
startDrawing,
endDrawing,
handleMouseMove,
handleMouseLeave,
handleMouseEnter,
saveImage,
resetCanvas,
creatOffscreenCanvas,
generateMask,
getTransformLineWidth,
getTransformedPoint,
setStrokes,
setTransform,
clearOverlayCanvas,
clearCanvas,
fitView
} = useDrawing({
lineWidth: lineWidth,
translatePos: translatePos,
isDisabled: isDisabled,
invertMask,
maskUpload,
onSave: onSave
});
const { handleOnWheel, setActiveScale, activeScale } = useZoom({
overlayCanvasRef,
canvasRef,
offscreenCanvasRef,
cursorRef,
lineWidth,
translatePos,
autoScale,
baseScale,
isLoadingMaskRef
});
const disabled = useMemo(() => {
return isDisabled || invertMask || !!maskUpload?.length;
}, [isDisabled, invertMask, maskUpload]);
// update the canvas size
const updateCanvasSize = useCallback(() => {
const canvas = canvasRef.current!;
const overlayCanvas = overlayCanvasRef.current!;
const offscreenCanvas = offscreenCanvasRef.current!;
overlayCanvas.width = canvas.width;
overlayCanvas.height = canvas.height;
offscreenCanvas.width = canvas.width;
offscreenCanvas.height = canvas.height;
}, []);
const downloadMask = useCallback(() => {
const mask = generateMask();
const link = document.createElement('a');
link.download = `mask_${dayjs().format('YYYYMMDDHHmmss')}.png`;
link.href = mask || '';
link.click();
}, [generateMask]);
const onReset = useCallback(() => {
clearOverlayCanvas();
setStrokes([]);
setMaskStrokes([]);
saveImage();
currentStroke.current = [];
console.log('Resetting strokes', currentStroke.current);
}, []);
const loadMaskPixs = (maskStrokes: Stroke[], mainStrokes?: Stroke[]) => {
try {
if (!maskStrokes.length && !mainStrokes?.length) {
return;
}
isLoadingMaskRef.current = true;
const offscreenCanvas = new OffscreenCanvas(
overlayCanvasRef.current!.width,
overlayCanvasRef.current!.height
);
if (!loadMaksWorkerRef.current) {
loadMaksWorkerRef.current = new Worker(
new URL('./offscreen-worker.ts', import.meta.url),
{
type: 'module'
}
);
}
loadMaksWorkerRef.current!.onmessage = (event: any) => {
if (event.data.type === 'done' && event.data.imageData) {
console.log('load mask done');
// draw the data to the overlay canvas
const ctx = overlayCanvasRef.current!.getContext('2d')!;
ctx.putImageData(event.data.imageData, 0, 0);
isLoadingMaskRef.current = false;
saveImage();
}
};
// send offscreen canvas to worker
loadMaksWorkerRef.current?.postMessage(
{ canvas: offscreenCanvas, type: 'init' },
[offscreenCanvas]
);
// send draw data to worker
loadMaksWorkerRef.current?.postMessage({
type: 'draw',
maskStrokes: maskStrokes,
strokes: mainStrokes || []
});
} catch (error) {
console.log('error---', error);
}
};
const redrawStrokes = async (strokes: Stroke[]) => {
if (!strokes.length && !maskStorkeRef.current.length) {
clearOverlayCanvas();
return;
}
loadMaskPixs(maskStorkeRef.current, strokes);
};
const undo = useCallback(() => {
if (
strokesRef.current.length === 0 &&
maskStorkeRef.current.length === 0
) {
clearOverlayCanvas();
return;
}
const lastlength = strokesRef.current.length;
const newStrokes = strokesRef.current.slice(0, -1);
setStrokes(newStrokes);
if (
!newStrokes.length &&
lastlength === 0 &&
maskStorkeRef.current.length
) {
setMaskStrokes([]);
}
clearTimeout(timer.current);
timer.current = setTimeout(() => {
redrawStrokes(newStrokes);
}, 100);
}, []);
const downloadOriginImage = () => {
const canvas = canvasRef.current!;
const link = document.createElement('a');
link.download = `image_${dayjs().format('YYYYMMDDHHmmss')}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
link.remove();
};
const downloadNewImage = () => {
if (!imageSrc) return;
const img = new Image();
img.src = imageSrc;
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = imageStatus.width;
canvas.height = imageStatus.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
const url = canvas.toDataURL('image/png');
const filename = `${canvas.width}x${canvas.height}_${dayjs().format('YYYYMMDDHHmmss')}.png`;
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
};
};
const download = () => {
if (imageStatus.isOriginal) {
downloadOriginImage();
} else {
downloadNewImage();
}
};
const drawImage = useCallback(async () => {
if (!containerRef.current || !canvasRef.current) return;
return new Promise<void>((resolve) => {
const img = new Image();
img.src = imageSrc;
img.onload = () => {
const canvas = canvasRef.current!;
const ctx = canvas!.getContext('2d');
const container = containerRef.current;
baseScale.current = Math.min(
container!.offsetWidth / img.width,
container!.offsetHeight / img.height,
1
);
// if need to fit the image to the container, show * baseScale.current
canvas!.width = img.width;
canvas!.height = img.height;
// fit the image to the container
autoScale.current = 1;
creatOffscreenCanvas();
updateCanvasSize();
clearCanvas();
ctx!.drawImage(img, 0, 0, canvas!.width, canvas!.height);
resolve();
};
});
}, [imageSrc]);
const invertPainting = (isChecked: boolean) => {
const ctx = overlayCanvasRef.current!.getContext('2d');
if (!ctx) return;
if (isChecked) {
const canvasWidth = overlayCanvasRef.current!.width;
const canvasHeight = overlayCanvasRef.current!.height;
clearOverlayCanvas();
const offscreenCanvas = new OffscreenCanvas(
overlayCanvasRef.current!.width,
overlayCanvasRef.current!.height
);
if (!invertWorkerRef.current) {
invertWorkerRef.current = new Worker(
new URL('./invert-worker.ts', import.meta.url),
{
type: 'module'
}
);
}
invertWorkerRef.current.onmessage = (event: any) => {
if (event.data.type === 'done' && event.data.imageData) {
// draw the data to the overlay canvas
const ctx = overlayCanvasRef.current!.getContext('2d')!;
ctx.putImageData(event.data.imageData, 0, 0);
isLoadingMaskRef.current = false;
saveImage();
}
};
// send offscreen canvas to worker
invertWorkerRef.current?.postMessage(
{ canvas: offscreenCanvas, type: 'init' },
[offscreenCanvas]
);
// send draw data to worker
const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
invertWorkerRef.current?.postMessage({
type: 'draw',
width: imageData.width,
height: imageData.height,
strokes: strokesRef.current,
maskStrokes: maskStorkeRef.current
});
} else {
redrawStrokes(strokesRef.current);
}
};
const updateCursorSize = () => {
cursorRef.current!.style.width = `${lineWidth * autoScale.current}px`;
cursorRef.current!.style.height = `${lineWidth * autoScale.current}px`;
};
const initializeImage = useCallback(async () => {
await drawImage();
onScaleImageSize?.({
width: canvasRef.current!.width,
height: canvasRef.current!.height
});
if (imageStatus.isResetNeeded) {
onReset();
resetCanvas();
} else if (
(strokesRef.current.length || maskStorkeRef.current.length) &&
imageStatus.isOriginal &&
!negativeMaskRef.current
) {
redrawStrokes(strokesRef.current);
} else if (
(strokesRef.current.length || maskStorkeRef.current.length) &&
imageStatus.isOriginal &&
negativeMaskRef.current
) {
invertPainting(true);
}
console.log('Image status:', imageStatus, negativeMaskRef.current);
updateCursorSize();
}, [drawImage, imageStatus.isOriginal, imageStatus.isResetNeeded]);
const handleFitView = () => {
fitView();
setActiveScale(autoScale.current);
updateCursorSize();
redrawStrokes(strokesRef.current);
};
const handleBrushSizeChange = (value: number) => {
setLineWidth(value);
cursorRef.current!.style.width = `${value}px`;
cursorRef.current!.style.height = `${value}px`;
};
const handleOnChangeMask = (e: any) => {
negativeMaskRef.current = e.target.checked;
invertPainting(e.target.checked);
setInvertMask(e.target.checked);
};
useEffect(() => {
initializeImage();
}, [initializeImage]);
useEffect(() => {
const handleUndoShortcut = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
undo();
}
};
const handleMouseDown = (e: MouseEvent) => {
mouseDownState.current = true;
};
const handleMouseUp = (e: MouseEvent) => {
mouseDownState.current = false;
};
window.addEventListener('keydown', handleUndoShortcut);
// mouse down
window.addEventListener('mousedown', handleMouseDown);
// mouse up
window.addEventListener('mouseup', handleMouseUp);
return () => {
clearTimeout(timer.current);
window.removeEventListener('keydown', handleUndoShortcut);
window.removeEventListener('mousedown', handleMouseDown);
window.removeEventListener('mouseup', handleMouseUp);
};
}, []);
const handleDeleteMask = () => {
setInvertMask(false);
redrawStrokes(strokesRef.current);
saveImage();
};
useImperativeHandle(ref, () => ({
clearMask: handleDeleteMask,
loadMaskPixs: async function (strokes: Stroke[]) {
setMaskStrokes(strokes);
setStrokes([]);
setTransform();
clearOverlayCanvas();
loadMaskPixs(strokes);
}
}));
useEffect(() => {
invertWorkerRef.current = new Worker(
new URL('./invert-worker.ts', import.meta.url),
{
type: 'module'
}
);
loadMaksWorkerRef.current = new Worker(
new URL('./offscreen-worker.ts', import.meta.url),
{
type: 'module'
}
);
return () => {
invertWorkerRef.current?.terminate();
loadMaksWorkerRef.current?.terminate();
};
}, []);
useEffect(() => {
if (maskUpload?.length) {
// clear the overlay canvas
clearOverlayCanvas();
}
}, [maskUpload]);
useEffect(() => {
if (disabled && cursorRef.current) {
cursorRef.current!.style.display = 'none';
}
}, [disabled]);
return (
<div className="editor-wrapper">
<div className="flex-between">
<ToolsBar
handleBrushSizeChange={handleBrushSizeChange}
undo={undo}
onClear={onReset}
handleFitView={handleFitView}
handleUpdateImageList={handleUpdateImageList}
handleUpdateMaskList={handleUpdateMaskList}
disabled={disabled}
lineWidth={lineWidth}
invertMask={invertMask}
loading={loading}
accept={accept}
></ToolsBar>
<ImageActionsBar
handleOnChangeMask={handleOnChangeMask}
invertMask={invertMask}
downloadMask={downloadMask}
download={download}
isOriginal={imageStatus.isOriginal}
disabled={isDisabled || !!maskUpload?.length}
maskUpload={maskUpload}
></ImageActionsBar>
</div>
<div
className="editor-content"
ref={containerRef}
style={{
position: 'relative',
width: '100%',
height: '100%',
flex: 1
}}
>
{loadingSize.loading && (
<Loading
width={loadingSize.width}
height={loadingSize.height}
></Loading>
)}
<canvas ref={canvasRef} style={{ position: 'absolute', zIndex: 1 }} />
<canvas
ref={overlayCanvasRef}
className={classNames('overlay-canvas', {
'overlay-canvas--disabled': disabled
})}
style={{ position: 'absolute', zIndex: 10, cursor: 'none' }}
onMouseDown={(event) => {
if (isLoadingMaskRef.current) {
return;
}
mouseDownState.current = true;
startDrawing(event);
}}
onMouseUp={(event) => {
if (isLoadingMaskRef.current) {
return;
}
mouseDownState.current = false;
endDrawing(event);
}}
onMouseEnter={handleMouseEnter}
onWheel={handleOnWheel}
onMouseMove={(e) => {
if (isLoadingMaskRef.current) {
return;
}
handleMouseMove(e);
draw(e);
}}
onMouseLeave={(e) => {
if (isLoadingMaskRef.current) {
return;
}
endDrawing(e);
handleMouseLeave();
}}
/>
<div
ref={cursorRef}
style={{
display: 'none',
position: 'fixed',
width: lineWidth * activeScale,
height: lineWidth * activeScale,
backgroundColor: COLOR,
borderRadius: '50%',
pointerEvents: 'none',
cursor: 'none',
zIndex: 5
}}
/>
</div>
</div>
);
}
);
export default React.memo(CanvasImageEditor);
@@ -0,0 +1,54 @@
/// <reference lib="webworker" />
let offscreenCanvas: OffscreenCanvas;
let ctx: OffscreenCanvasRenderingContext2D;
const COLOR = 'rgba(0, 0, 255, 0.3)';
type Point = { x: number; y: number; lineWidth: number };
type Stroke = Point[];
self.onmessage = (event) => {
const { width, height, strokes, maskStrokes } = event.data;
if (event.data.type === 'init') {
offscreenCanvas = event.data.canvas;
ctx = offscreenCanvas!.getContext('2d')!;
return;
}
if (event.data.type === 'draw') {
ctx.fillStyle = COLOR;
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = 'destination-out';
[...strokes].forEach((stroke: Stroke) => {
stroke.forEach((point: { x: number; y: number; lineWidth: number }) => {
const lineWidth = point.lineWidth;
ctx.fillStyle = 'rgba(0,0,0,1)';
ctx.beginPath();
ctx.arc(point.x, point.y, lineWidth / 2, 0, Math.PI * 2);
ctx.fill();
});
});
// points
maskStrokes?.forEach((stroke: Stroke) => {
stroke.forEach((point: { x: number; y: number; lineWidth: number }) => {
const lineWidth = 4;
ctx.fillStyle = 'rgba(0,0,0,1)';
ctx.beginPath();
ctx.arc(point.x, point.y, lineWidth / 2, 0, Math.PI * 2);
ctx.fill();
});
});
const newImageData = ctx.getImageData(0, 0, width, height);
self.postMessage({ type: 'done', imageData: newImageData }, [
newImageData.data.buffer
]);
}
};
export {};
@@ -0,0 +1,111 @@
let offscreenCanvas: OffscreenCanvas;
let ctx: OffscreenCanvasRenderingContext2D;
const COLOR = 'rgba(0, 0, 255, 0.3)';
type Point = { x: number; y: number; lineWidth: number };
type Stroke = Point[];
const postDone = () => {
const imageData = ctx.getImageData(
0,
0,
offscreenCanvas.width,
offscreenCanvas.height
);
self.postMessage({ type: 'done', imageData });
};
const drawFillRect = (
ctx: OffscreenCanvasRenderingContext2D,
stroke: Stroke,
options: any
) => {
const { color } = options;
stroke?.forEach(({ x, y }) => {
const width = options.lineWidth || 10;
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,1)';
ctx.globalCompositeOperation = 'destination-out';
ctx.fillRect(x - width / 2, y - width / 2, width, width);
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = color;
ctx.fillRect(x - width / 2, y - width / 2, width, width);
ctx.restore();
});
};
const drawStrokes = (strokes: Stroke[]) => {
strokes?.forEach((stroke) => {
drawFillRect(ctx, stroke, {
color: COLOR
});
});
};
const drawLine = (
stroke: Stroke | Point[],
options: {
lineWidth?: number;
color: string;
compositeOperation: 'source-over' | 'destination-out';
}
) => {
const { color, compositeOperation } = options;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.globalCompositeOperation = compositeOperation;
ctx.save();
ctx.beginPath();
stroke?.forEach((point, i) => {
const { x, y } = point;
ctx.lineWidth = point.lineWidth || 10;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
if (compositeOperation === 'source-over') {
ctx.strokeStyle = color;
}
ctx.stroke();
ctx.restore();
};
const drawLines = (strokes: Stroke[]) => {
strokes?.forEach((stroke: Point[], index) => {
drawLine(stroke, {
color: COLOR,
compositeOperation: 'destination-out'
});
drawLine(stroke, {
color: COLOR,
compositeOperation: 'source-over'
});
});
};
self.onmessage = (event) => {
if (event.data.type === 'init') {
offscreenCanvas = event.data.canvas;
ctx = offscreenCanvas!.getContext('2d')!;
return;
}
if (event.data.type === 'draw') {
ctx?.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
const { maskStrokes, strokes } = event.data;
drawStrokes(maskStrokes);
drawLines(strokes);
postDone();
}
};
export {};
+198
View File
@@ -0,0 +1,198 @@
import IconFont from '@/components/icon-font';
import { KeyMap } from '@/config/hotkeys';
import UploadImg from '@/pages/playground/components/upload-img';
import {
ClearOutlined,
DownloadOutlined,
ExpandOutlined,
FormatPainterOutlined,
UndoOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Slider, Tooltip } from 'antd';
import { CheckboxChangeEvent } from 'antd/es/checkbox';
import React from 'react';
interface ToolsBarProps {
disabled: boolean;
loading: boolean;
lineWidth: number;
uploadButton?: React.ReactNode;
invertMask?: boolean;
accept?: string;
handleBrushSizeChange: (value: number) => void;
undo: () => void;
onClear: () => void;
handleFitView: () => void;
handleUpdateImageList: (fileList: any[]) => void;
handleUpdateMaskList: (fileList: any[]) => void;
}
const ToolsBar: React.FC<ToolsBarProps> = (props) => {
const {
disabled,
loading,
lineWidth,
invertMask,
accept,
handleBrushSizeChange,
undo,
onClear,
handleFitView,
handleUpdateImageList,
handleUpdateMaskList
} = props;
const intl = useIntl();
return (
<div className="tools">
<Tooltip
placement="bottomLeft"
arrow={false}
overlayInnerStyle={{
background: 'var(--color-white-1)',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
width: 160
}}
title={
<div className="flex-column" style={{ width: '100%' }}>
<span className="text-secondary">
{intl.formatMessage({ id: 'playground.image.brushSize' })}
</span>
<Slider
disabled={disabled}
style={{ marginBlock: '4px 6px', marginLeft: 0, flex: 1 }}
vertical={false}
defaultValue={lineWidth}
min={10}
max={100}
onChange={handleBrushSizeChange}
/>
</div>
}
>
<Button size="middle" type="text">
<FormatPainterOutlined className="font-size-14" />
</Button>
</Tooltip>
<Tooltip
title={
<span>
[{KeyMap.UNDO.textKeybinding}]
<span className="m-l-5">
{intl.formatMessage({ id: 'common.button.undo' })}
</span>
</span>
}
>
<Button onClick={undo} size="middle" type="text" disabled={disabled}>
<UndoOutlined className="font-size-14" />
</Button>
</Tooltip>
<Tooltip title={intl.formatMessage({ id: 'common.button.clear' })}>
<Button onClick={onClear} size="middle" type="text" disabled={disabled}>
<ClearOutlined className="font-size-14" />
</Button>
</Tooltip>
<UploadImg
disabled={loading || invertMask}
handleUpdateImgList={handleUpdateImageList}
size="middle"
accept={accept}
></UploadImg>
<UploadImg
title={intl.formatMessage({
id: 'playground.image.mask.upload'
})}
icon={<IconFont type="icon-mosaic-2"></IconFont>}
disabled={loading || invertMask}
handleUpdateImgList={handleUpdateMaskList}
size="middle"
accept={accept}
></UploadImg>
<Tooltip title={intl.formatMessage({ id: 'playground.image.fitview' })}>
<Button
onClick={handleFitView}
size="middle"
type="text"
disabled={loading}
>
<ExpandOutlined className="font-size-14" />
</Button>
</Tooltip>
</div>
);
};
interface ImageActionsBarProps {
disabled: boolean;
maskUpload?: any[];
isOriginal: boolean;
invertMask: boolean;
handleOnChangeMask: (e: CheckboxChangeEvent) => void;
downloadMask: () => void;
download: () => void;
}
const ImageActionsBar: React.FC<ImageActionsBarProps> = (props) => {
const intl = useIntl();
const {
disabled,
isOriginal,
invertMask,
handleOnChangeMask,
downloadMask,
download
} = props;
return (
<div className="tools">
{isOriginal && (
<>
<Tooltip
title={
<span style={{ whiteSpace: 'pre-wrap' }}>
{intl.formatMessage({
id: 'playground.image.negativeMask.tips'
})}
</span>
}
>
<Checkbox
onChange={handleOnChangeMask}
className="flex-center"
checked={invertMask}
disabled={disabled}
>
<span className="font-size-12">
{intl.formatMessage({
id: 'playground.image.negativeMask'
})}
</span>
</Checkbox>
</Tooltip>
<Tooltip
title={intl.formatMessage({
id: 'playground.image.saveMask'
})}
>
<Button onClick={downloadMask} size="middle" type="text">
<IconFont className="font-size-14" type="icon-save1"></IconFont>
</Button>
</Tooltip>
</>
)}
{!isOriginal && (
<Tooltip
title={intl.formatMessage({ id: 'playground.image.download' })}
>
<Button onClick={download} size="middle" type="text">
<DownloadOutlined className="font-size-14" />
</Button>
</Tooltip>
)}
</div>
);
};
export { ImageActionsBar, ToolsBar };
+39 -2
View File
@@ -1,3 +1,4 @@
import { useIntl } from '@umijs/max';
import _ from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import Inner from './inner';
@@ -8,15 +9,20 @@ interface LabelSelectorProps {
btnText?: string;
description?: React.ReactNode;
onChange?: (labels: Record<string, any>) => void;
onBlur?: (e: any, type: string, index: number) => void;
onDelete?: (index: number) => void;
}
const LabelSelector: React.FC<LabelSelectorProps> = ({
labels,
onChange,
onBlur,
onDelete,
label,
btnText,
description
}) => {
const intl = useIntl();
const [labelsData, setLabelsData] = useState({});
const [labelList, setLabelList] = useState<{ key: string; value: string }[]>(
[]
@@ -33,7 +39,6 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
});
setLabelList(list);
}
console.log('labelsData=========', labelsData);
}, [labels]);
const handleLabelListChange = useCallback(
@@ -43,19 +48,51 @@ const LabelSelector: React.FC<LabelSelectorProps> = ({
[setLabelList]
);
const handleLabelsChange = (data: Record<string, any>) => {
console.log('handleLabelsChange', data);
setLabelsData(data);
onChange?.(data);
};
const handleOnPaste = (
e: React.ClipboardEvent<HTMLTextAreaElement>,
index: number
) => {
const clipboardText = e.clipboardData.getData('text');
if (!clipboardText || clipboardText.indexOf('=') === -1) return;
e.preventDefault();
const lines = clipboardText
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line && line.includes('='));
const parsedData = lines.map((line) => {
const [key, value] = line.split('=').map((part) => part.trim());
return { key, value };
});
console.log(lines, parsedData);
setLabelList((prevPairs) => {
const newPairs = [...prevPairs];
newPairs.splice(index, 1, ...parsedData);
return newPairs;
});
};
return (
<Inner
label={label}
btnText={btnText}
description={description}
description={
description ?? intl.formatMessage({ id: 'models.form.keyvalue.paste' })
}
labels={labelsData}
labelList={labelList}
onChange={handleLabelsChange}
onLabelListChange={handleLabelListChange}
onPaste={handleOnPaste}
onBlur={onBlur}
onDelete={onDelete}
/>
);
};
+14 -9
View File
@@ -12,6 +12,9 @@ interface LabelSelectorProps {
labelList: Array<{ key: string; value: string }>;
onLabelListChange: (list: { key: string; value: string }[]) => void;
onChange?: (labels: Record<string, any>) => void;
onPaste?: (e: any, index: number) => void;
onBlur?: (e: any, type: string, index: number) => void;
onDelete?: (index: number) => void;
description?: React.ReactNode;
}
@@ -20,12 +23,16 @@ const Inner: React.FC<LabelSelectorProps> = ({
labelList,
onChange,
onLabelListChange,
onPaste,
onBlur,
onDelete,
label,
btnText,
description
}) => {
const intl = useIntl();
const buttonRef = useRef<HTMLButtonElement>(null);
const boxRef = useRef<HTMLDivElement>(null);
const updateLabels = (list: { key: string; value: string }[]) => {
const newLabels = _.reduce(
@@ -40,7 +47,7 @@ const Inner: React.FC<LabelSelectorProps> = ({
);
onChange?.(newLabels);
};
const handleOnChange = (index: string, label: any) => {
const handleOnChange = (index: number, label: any) => {
const list = _.cloneDeep(labelList);
list[index] = label;
onLabelListChange(list);
@@ -57,24 +64,20 @@ const Inner: React.FC<LabelSelectorProps> = ({
];
onLabelListChange(newLabelList);
updateLabels(newLabelList);
setTimeout(() => {
// button scroll to view
buttonRef.current?.scrollIntoView?.({ behavior: 'smooth' });
}, 100);
};
const handleOnDelete = (index: string) => {
const handleOnDelete = (index: number) => {
const list = _.cloneDeep(labelList);
list.splice(parseInt(index), 1);
list.splice(index, 1);
onLabelListChange(list);
updateLabels(list);
onDelete?.(index);
};
return (
<Wrapper label={label} description={description}>
<>
{_.map(labelList, (item: any, index: string) => {
{labelList?.map((item: any, index: number) => {
return (
<LabelItem
key={index}
@@ -83,6 +86,8 @@ const Inner: React.FC<LabelSelectorProps> = ({
labelList={labelList}
onDelete={() => handleOnDelete(index)}
onChange={(obj) => handleOnChange(index, obj)}
onPaste={(e) => onPaste?.(e, index)}
onBlur={(e: any, type: string) => onBlur?.(e, type, index)}
/>
);
})}
+10 -3
View File
@@ -18,6 +18,8 @@ interface LabelItemProps {
onDelete?: () => void;
labelList: { key: string; value: string }[];
onChange?: (params: { key: string; value: string }) => void;
onPaste?: (e: any) => void;
onBlur?: (e: any, type: string) => void;
}
const LabelItem: React.FC<LabelItemProps> = ({
label,
@@ -26,7 +28,9 @@ const LabelItem: React.FC<LabelItemProps> = ({
keyAddon,
valueAddon,
onChange,
onDelete
onDelete,
onPaste,
onBlur
}) => {
const intl = useIntl();
const [open, setOpen] = useState(false);
@@ -47,7 +51,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
});
};
const handleKeyOnBlur = (e: any) => {
const handleKeyOnBlur = (e: any, type: string) => {
const val = e.target.value;
// has duplicate key
const duplicates = _.filter(
@@ -66,6 +70,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
} else {
setOpen(false);
}
onBlur?.(e, type);
};
return (
@@ -81,7 +86,8 @@ const LabelItem: React.FC<LabelItemProps> = ({
label={intl.formatMessage({ id: 'common.input.key' })}
value={label.key}
onChange={handleOnKeyChange}
onBlur={handleKeyOnBlur}
onBlur={(e: any) => handleKeyOnBlur(e, 'key')}
onPaste={onPaste}
></SealInput.Input>
</Tooltip>
)}
@@ -94,6 +100,7 @@ const LabelItem: React.FC<LabelItemProps> = ({
label={intl.formatMessage({ id: 'common.input.value' })}
value={label.value}
onChange={handleOnValueChange}
onBlur={(e: any) => onBlur?.(e, 'value')}
></SealInput.Input>
)}
</div>
+8 -3
View File
@@ -3,15 +3,20 @@ import React from 'react';
import styles from './styles/wrapper.less';
const Wrapper: React.FC<{
label?: string;
label?: React.ReactNode;
description?: React.ReactNode;
labelExtra?: React.ReactNode;
children: React.ReactNode;
}> = ({ children, label, description, ...rest }) => {
}> = ({ children, label, description, labelExtra, ...rest }) => {
return (
<div className={styles['wrapper']}>
{label && (
<span className="label">
<LabelInfo label={label} description={description}></LabelInfo>
<LabelInfo
label={label}
description={description}
labelExtra={labelExtra}
></LabelInfo>
</span>
)}
{React.isValidElement(children)

Some files were not shown because too many files have changed in this diff Show More