diff --git a/app/package.json b/app/package.json
index 509ac210..fbbc4514 100644
--- a/app/package.json
+++ b/app/package.json
@@ -40,6 +40,7 @@
"localforage": "^1.10.0",
"lucide-react": "^0.289.0",
"match-sorter": "^6.3.1",
+ "next-themes": "^0.2.1",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-chartjs-2": "^5.2.0",
@@ -53,6 +54,7 @@
"remark-breaks": "^4.0.0",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
+ "sonner": "^1.3.1",
"sort-by": "^1.2.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml
index 82aaaa24..ac9940d1 100644
--- a/app/pnpm-lock.yaml
+++ b/app/pnpm-lock.yaml
@@ -89,6 +89,9 @@ dependencies:
match-sorter:
specifier: ^6.3.1
version: 6.3.1
+ next-themes:
+ specifier: ^0.2.1
+ version: 0.2.1(next@14.0.4)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
@@ -128,6 +131,9 @@ dependencies:
remark-math:
specifier: ^5.1.1
version: 5.1.1
+ sonner:
+ specifier: ^1.3.1
+ version: 1.3.1(react-dom@18.2.0)(react@18.2.0)
sort-by:
specifier: ^1.2.0
version: 1.2.0
@@ -543,6 +549,91 @@ packages:
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
dev: false
+ /@next/env@14.0.4:
+ resolution: {integrity: sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==}
+ dev: false
+
+ /@next/swc-darwin-arm64@14.0.4:
+ resolution: {integrity: sha512-mF05E/5uPthWzyYDyptcwHptucf/jj09i2SXBPwNzbgBNc+XnwzrL0U6BmPjQeOL+FiB+iG1gwBeq7mlDjSRPg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-darwin-x64@14.0.4:
+ resolution: {integrity: sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-arm64-gnu@14.0.4:
+ resolution: {integrity: sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-arm64-musl@14.0.4:
+ resolution: {integrity: sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-x64-gnu@14.0.4:
+ resolution: {integrity: sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-x64-musl@14.0.4:
+ resolution: {integrity: sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-win32-arm64-msvc@14.0.4:
+ resolution: {integrity: sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-win32-ia32-msvc@14.0.4:
+ resolution: {integrity: sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-win32-x64-msvc@14.0.4:
+ resolution: {integrity: sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: false
+ optional: true
+
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@@ -1860,6 +1951,12 @@ packages:
resolution: {integrity: sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==}
dev: true
+ /@swc/helpers@0.5.2:
+ resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
+ dependencies:
+ tslib: 2.6.2
+ dev: false
+
/@swc/types@0.1.5:
resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==}
dev: true
@@ -2539,6 +2636,13 @@ packages:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: true
+ /busboy@1.6.0:
+ resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
+ engines: {node: '>=10.16.0'}
+ dependencies:
+ streamsearch: 1.1.0
+ dev: false
+
/callsites@3.1.0:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
@@ -2557,7 +2661,6 @@ packages:
/caniuse-lite@1.0.30001554:
resolution: {integrity: sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ==}
- dev: true
/ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -2626,6 +2729,10 @@ packages:
source-map: 0.6.1
dev: true
+ /client-only@0.0.1:
+ resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+ dev: false
+
/clsx@2.0.0:
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
engines: {node: '>=6'}
@@ -3235,7 +3342,6 @@ packages:
/glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
- dev: true
/glob@7.1.6:
resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==}
@@ -3279,7 +3385,6 @@ packages:
/graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- dev: true
/graphemer@1.4.0:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
@@ -4276,6 +4381,58 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true
+ /next-themes@0.2.1(next@14.0.4)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
+ peerDependencies:
+ next: '*'
+ react: '*'
+ react-dom: '*'
+ dependencies:
+ next: 14.0.4(react-dom@18.2.0)(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
+ /next@14.0.4(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==}
+ engines: {node: '>=18.17.0'}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ react: ^18.2.0
+ react-dom: ^18.2.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ sass:
+ optional: true
+ dependencies:
+ '@next/env': 14.0.4
+ '@swc/helpers': 0.5.2
+ busboy: 1.6.0
+ caniuse-lite: 1.0.30001554
+ graceful-fs: 4.2.11
+ postcss: 8.4.31
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ styled-jsx: 5.1.1(react@18.2.0)
+ watchpack: 2.4.0
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 14.0.4
+ '@next/swc-darwin-x64': 14.0.4
+ '@next/swc-linux-arm64-gnu': 14.0.4
+ '@next/swc-linux-arm64-musl': 14.0.4
+ '@next/swc-linux-x64-gnu': 14.0.4
+ '@next/swc-linux-x64-musl': 14.0.4
+ '@next/swc-win32-arm64-msvc': 14.0.4
+ '@next/swc-win32-ia32-msvc': 14.0.4
+ '@next/swc-win32-x64-msvc': 14.0.4
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+ dev: false
+
/no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
dependencies:
@@ -5063,6 +5220,16 @@ packages:
engines: {node: '>=8'}
dev: true
+ /sonner@1.3.1(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/sort-by@1.2.0:
resolution: {integrity: sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==}
dependencies:
@@ -5093,6 +5260,11 @@ packages:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
dev: false
+ /streamsearch@1.1.0:
+ resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
+ engines: {node: '>=10.0.0'}
+ dev: false
+
/strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -5111,6 +5283,23 @@ packages:
inline-style-parser: 0.1.1
dev: false
+ /styled-jsx@5.1.1(react@18.2.0):
+ resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
+ engines: {node: '>= 12.0.0'}
+ peerDependencies:
+ '@babel/core': '*'
+ babel-plugin-macros: '*'
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ babel-plugin-macros:
+ optional: true
+ dependencies:
+ client-only: 0.0.1
+ react: 18.2.0
+ dev: false
+
/sucrase@3.34.0:
resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==}
engines: {node: '>=8'}
@@ -5588,7 +5777,6 @@ packages:
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
- dev: true
/web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}
diff --git a/app/src/App.tsx b/app/src/App.tsx
index e3709e1c..08cf09b9 100644
--- a/app/src/App.tsx
+++ b/app/src/App.tsx
@@ -2,11 +2,13 @@ import { Provider } from "react-redux";
import store from "./store/index.ts";
import AppProvider from "./components/app/AppProvider.tsx";
import { AppRouter } from "./router.tsx";
+import { Toaster } from "@/components/ui/sonner";
function App() {
return (
+
);
diff --git a/app/src/admin/market.ts b/app/src/admin/market.ts
new file mode 100644
index 00000000..91c193f6
--- /dev/null
+++ b/app/src/admin/market.ts
@@ -0,0 +1,40 @@
+export const marketEditableTags = [
+ "official",
+ "unstable",
+ "web",
+ "high-quality",
+ "high-price",
+ "open-source",
+ "image-generation",
+ "multi-modal",
+ "fast",
+ "english-model",
+];
+
+export const modelImages = [
+ "gpt35turbo.png",
+ "gpt35turbo16k.webp",
+ "gpt4.png",
+ "gpt432k.webp",
+ "gpt4v.png",
+ "gpt4dalle.png",
+ "claude.png",
+ "claude100k.png",
+ "stablediffusion.jpeg",
+ "llama2.webp",
+ "llamacode.webp",
+ "dalle.jpeg",
+ "midjourney.jpg",
+ "newbing.jpg",
+ "palm2.webp",
+ "gemini.jpeg",
+ "chatglm.png",
+ "tongyi.png",
+ "sparkdesk.jpg",
+ "hunyuan.png",
+ "360gpt.png",
+ "baichuan.png",
+ "skylark.jpg",
+];
+
+export const marketTags = [...marketEditableTags, "free", "high-context"];
diff --git a/app/src/api/types.ts b/app/src/api/types.ts
index 3e06e501..7c490a58 100644
--- a/app/src/api/types.ts
+++ b/app/src/api/types.ts
@@ -13,9 +13,12 @@ export type Message = {
export type Model = {
id: string;
name: string;
+ description?: string;
free: boolean;
auth: boolean;
+ default: boolean;
high_context: boolean;
+ avatar: string;
tag?: string[];
};
diff --git a/app/src/assets/admin/all.less b/app/src/assets/admin/all.less
index 96398b6e..ba6094ed 100644
--- a/app/src/assets/admin/all.less
+++ b/app/src/assets/admin/all.less
@@ -1,5 +1,6 @@
@import "menu";
@import "dashboard";
+@import "market";
@import "management";
@import "broadcast";
@import "channel";
diff --git a/app/src/assets/admin/dashboard.less b/app/src/assets/admin/dashboard.less
index 712faddd..b3a7df3a 100644
--- a/app/src/assets/admin/dashboard.less
+++ b/app/src/assets/admin/dashboard.less
@@ -46,7 +46,6 @@
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
background: hsl(var(--background));
- border: 1px solid hsl(var(--border));
box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);
user-select: none;
max-width: 460px;
@@ -116,7 +115,6 @@
border-radius: var(--radius);
background: hsl(var(--background));
box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);
- border: 1px solid hsl(var(--border));
user-select: none;
@media (max-width: 680px) {
diff --git a/app/src/assets/admin/market.less b/app/src/assets/admin/market.less
new file mode 100644
index 00000000..2f806f89
--- /dev/null
+++ b/app/src/assets/admin/market.less
@@ -0,0 +1,148 @@
+.market {
+ width: 100%;
+ height: max-content;
+ padding: 2rem;
+ display: flex;
+ flex-direction: column;
+
+ .market-card {
+ width: 100%;
+ height: 100%;
+ min-height: 20vh;
+ }
+
+ & > * {
+ margin-bottom: 0.5rem;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
+
+.market-list {
+ display: flex;
+ flex-direction: column;
+
+ & > * {
+ margin-bottom: 1rem;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ .market-item {
+ display: flex;
+ flex-direction: row;
+ padding: 1.5rem 1rem;
+ border-radius: var(--radius);
+ border: 1px solid hsl(var(--border));
+ user-select: none;
+ width: 100%;
+ height: max-content;
+ align-items: center;
+ background: hsl(var(--card));
+
+ .market-tags {
+ display: flex;
+ flex-direction: row;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ width: 100%;
+
+ .market-tag {
+ white-space: nowrap;
+ padding: 0.25rem 0.75rem !important;
+ }
+ }
+
+ .market-images {
+ display: flex;
+ flex-direction: row;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ width: 100%;
+
+ .market-image {
+ width: 2.5rem;
+ height: 2.5rem;
+ padding: 0.25rem;
+ transition: 0.1s;
+
+ img {
+ width: 2rem;
+ height: 2rem;
+ opacity: 0.6;
+ border-radius: calc(var(--radius) - 2px);
+ transition: 0.1s;
+ }
+
+ &.active {
+ img {
+ opacity: 1;
+ }
+ }
+ }
+ }
+
+ svg {
+ flex-shrink: 0;
+ }
+
+ .drop-icon {
+ color: hsl(var(--text-secondary));
+ transition: color 0.25s ease;
+ }
+
+ &:hover {
+ .drop-icon {
+ color: hsl(var(--text));
+ }
+ }
+
+ .model-wrapper {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ flex-basis: 0;
+
+ & > * {
+ margin-bottom: 1rem;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ .market-row {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ width: 100%;
+
+ & > span {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ font-size: 0.9rem;
+ white-space: nowrap;
+ min-width: 68px;
+ text-align: center;
+
+ svg {
+ transform: translateY(1px);
+ }
+ }
+
+ & > * {
+ margin-right: 0.75rem;
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/assets/pages/home.less b/app/src/assets/pages/home.less
index 7ca9f85e..d0704770 100644
--- a/app/src/assets/pages/home.less
+++ b/app/src/assets/pages/home.less
@@ -99,6 +99,7 @@
animation: fadein 0.25s forwards ease-in-out;
opacity: 0;
width: calc(100% - 1rem);
+ background: hsla(var(--background-container));
@keyframes fadein {
from { opacity: 0; transform: translateY(2.5rem); }
diff --git a/app/src/components/admin/MenuBar.tsx b/app/src/components/admin/MenuBar.tsx
index 4f437661..b0803bd4 100644
--- a/app/src/components/admin/MenuBar.tsx
+++ b/app/src/components/admin/MenuBar.tsx
@@ -2,6 +2,7 @@ import { useDispatch, useSelector } from "react-redux";
import { closeMenu, selectMenu } from "@/store/menu.ts";
import React, { useMemo } from "react";
import {
+ BookCopy,
CandlestickChart,
GitFork,
LayoutDashboard,
@@ -54,6 +55,11 @@ function MenuBar() {
path={"/"}
/>
} path={"/users"} />
+ }
+ path={"/market"}
+ />
}
diff --git a/app/src/components/home/ModelMarket.tsx b/app/src/components/home/ModelMarket.tsx
index 646aba84..950376aa 100644
--- a/app/src/components/home/ModelMarket.tsx
+++ b/app/src/components/home/ModelMarket.tsx
@@ -11,7 +11,7 @@ import {
X,
} from "lucide-react";
import React, { useMemo, useState } from "react";
-import { modelAvatars, supportModels } from "@/conf.ts";
+import { supportModels } from "@/conf.ts";
import { splitList } from "@/utils/base.ts";
import { Model } from "@/api/types.ts";
import { useDispatch, useSelector } from "react-redux";
@@ -103,10 +103,7 @@ function ModelItem({
return getPlanModels(level).includes(model.id);
}, [model, level, student]);
- const avatar = useMemo(() => {
- const source = modelAvatars[model.id] || modelAvatars[supportModels[0].id];
- return `/icons/${source}`;
- }, [model]);
+ const avatar = useMemo(() => `/icons/${model.avatar}`, [model]);
return (
;
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme();
+
+ return (
+
+ );
+};
+
+export { Toaster };
diff --git a/app/src/conf.ts b/app/src/conf.ts
index 5b8a92b9..3c52c09a 100644
--- a/app/src/conf.ts
+++ b/app/src/conf.ts
@@ -20,135 +20,168 @@ export const deploy: boolean = true;
export let rest_api: string = getRestApi(deploy);
export let ws_api: string = getWebsocketApi(deploy);
export const tokenField = getTokenField(deploy);
+
export let supportModels: Model[] = loadPreferenceModels([
// openai models
{
id: "gpt-3.5-turbo-0613",
name: "GPT-3.5",
+ avatar: "gpt35turbo.png",
free: true,
auth: false,
high_context: false,
+ default: true,
tag: ["free", "official"],
},
{
id: "gpt-3.5-turbo-16k-0613",
name: "GPT-3.5-16k",
+ avatar: "gpt35turbo16k.webp",
free: true,
auth: true,
high_context: true,
+ default: true,
tag: ["free", "official", "high-context"],
},
{
id: "gpt-3.5-turbo-1106",
name: "GPT-3.5 1106",
+ avatar: "gpt35turbo16k.webp",
free: true,
auth: true,
high_context: true,
+ default: true,
tag: ["free", "official"],
},
{
id: "gpt-3.5-turbo-fast",
name: "GPT-3.5 Fast",
+ avatar: "gpt35turbo16k.webp",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official"],
},
{
id: "gpt-3.5-turbo-16k-fast",
name: "GPT-3.5 16K Fast",
+ avatar: "gpt35turbo16k.webp",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official"],
},
{
id: "gpt-4-0613",
name: "GPT-4",
+ avatar: "gpt4.png",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "high-quality"],
},
{
id: "gpt-4-1106-preview",
name: "GPT-4 Turbo 128k",
+ avatar: "gpt432k.webp",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "high-context", "unstable"],
},
{
id: "gpt-4-vision-preview",
name: "GPT-4 Vision 128k",
+ avatar: "gpt4v.png",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "high-context", "multi-modal", "unstable"],
},
{
id: "gpt-4-v",
name: "GPT-4 Vision",
+ avatar: "gpt4v.png",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "unstable", "multi-modal"],
},
{
id: "gpt-4-dalle",
name: "GPT-4 DALLE",
+ avatar: "gpt4dalle.png",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "unstable", "image-generation"],
},
{
id: "azure-gpt-3.5-turbo",
name: "Azure GPT-3.5",
+ avatar: "gpt35turbo.png",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official"],
},
{
id: "azure-gpt-3.5-turbo-16k",
name: "Azure GPT-3.5 16K",
+ avatar: "gpt35turbo16k.webp",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official"],
},
{
id: "azure-gpt-4",
name: "Azure GPT-4",
+ avatar: "gpt4.png",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "high-quality"],
},
{
id: "azure-gpt-4-1106-preview",
name: "Azure GPT-4 Turbo 128k",
+ avatar: "gpt432k.webp",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "high-context", "unstable"],
},
{
id: "azure-gpt-4-vision-preview",
name: "Azure GPT-4 Vision 128k",
+ avatar: "gpt4v.png",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "high-context", "multi-modal"],
},
{
id: "azure-gpt-4-32k",
name: "Azure GPT-4 32k",
+ avatar: "gpt432k.webp",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "multi-modal"],
},
@@ -156,25 +189,31 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "spark-desk-v3",
name: "讯飞星火 V3",
+ avatar: "sparkdesk.jpg",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official", "high-quality"],
},
{
id: "spark-desk-v2",
name: "讯飞星火 V2",
+ avatar: "sparkdesk.jpg",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["official"],
},
{
id: "spark-desk-v1.5",
name: "讯飞星火 V1.5",
+ avatar: "sparkdesk.jpg",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["official"],
},
@@ -182,33 +221,41 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "qwen-plus-net",
name: "通义千问 Plus Net",
+ avatar: "tongyi.png",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official", "high-quality", "web"],
},
{
id: "qwen-plus",
name: "通义千问 Plus",
+ avatar: "tongyi.png",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official", "high-quality"],
},
{
id: "qwen-turbo-net",
name: "通义千问 Turbo Net",
+ avatar: "tongyi.png",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["official", "web"],
},
{
id: "qwen-turbo",
name: "通义千问 Turbo",
+ avatar: "tongyi.png",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["official"],
},
@@ -216,9 +263,11 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "hunyuan",
name: "腾讯混元 Pro",
+ avatar: "hunyuan.png",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official"],
},
@@ -226,9 +275,11 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "zhipu-chatglm-turbo",
name: "ChatGLM Turbo",
+ avatar: "chatglm.png",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "open-source", "high-context"],
},
@@ -236,9 +287,11 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "baichuan-53b",
name: "百川 Baichuan 53B",
+ avatar: "baichuan.png",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official", "open-source"],
},
@@ -246,9 +299,11 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "skylark-chat",
name: "抖音豆包 Skylark",
+ avatar: "skylark.jpg",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official"],
},
@@ -256,34 +311,42 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "360-gpt-v9",
name: "360 智脑",
+ avatar: "360gpt.png",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["official"],
},
{
id: "claude-1-100k",
name: "Claude",
+ avatar: "claude.png",
free: true,
auth: true,
high_context: true,
+ default: true,
tag: ["free", "unstable"],
},
{
id: "claude-2",
name: "Claude 100k",
+ avatar: "claude100k.png",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "high-context"],
},
{
id: "claude-2.1",
name: "Claude 200k",
+ avatar: "claude100k.png",
free: false,
auth: true,
high_context: true,
+ default: true,
tag: ["official", "high-context"],
},
@@ -291,50 +354,62 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "llama-2-70b",
name: "LLaMa-2 70B",
+ avatar: "llama2.webp",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["open-source", "unstable"],
},
{
id: "llama-2-13b",
name: "LLaMa-2 13B",
+ avatar: "llama2.webp",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["open-source", "unstable"],
},
{
id: "llama-2-7b",
name: "LLaMa-2 7B",
+ avatar: "llama2.webp",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["open-source", "unstable"],
},
{
id: "code-llama-34b",
name: "Code LLaMa 34B",
+ avatar: "llamacode.webp",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["open-source", "unstable"],
},
{
id: "code-llama-13b",
name: "Code LLaMa 13B",
+ avatar: "llamacode.webp",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["open-source", "unstable"],
},
{
id: "code-llama-7b",
name: "Code LLaMa 7B",
+ avatar: "llamacode.webp",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["open-source", "unstable"],
},
@@ -342,9 +417,11 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "bing-creative",
name: "New Bing",
+ avatar: "newbing.jpg",
free: true,
auth: true,
high_context: true,
+ default: true,
tag: ["free", "unstable", "web"],
},
@@ -352,9 +429,11 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "chat-bison-001",
name: "Google PaLM2",
+ avatar: "palm2.webp",
free: true,
auth: true,
high_context: false,
+ default: false,
tag: ["free", "english-model"],
},
@@ -362,17 +441,21 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "gemini-pro",
name: "Gemini Pro",
+ avatar: "gemini.jpeg",
free: true,
auth: true,
high_context: true,
+ default: true,
tag: ["free", "official"],
},
{
id: "gemini-pro-vision",
name: "Gemini Pro Vision",
+ avatar: "gemini.jpeg",
free: true,
auth: true,
high_context: true,
+ default: true,
tag: ["free", "official", "multi-modal"],
},
@@ -380,96 +463,76 @@ export let supportModels: Model[] = loadPreferenceModels([
{
id: "midjourney",
name: "Midjourney",
+ avatar: "midjourney.jpg",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official", "image-generation"],
},
{
id: "midjourney-fast",
name: "Midjourney Fast",
+ avatar: "midjourney.jpg",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official", "fast", "image-generation"],
},
{
id: "midjourney-turbo",
name: "Midjourney Turbo",
+ avatar: "midjourney.jpg",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official", "fast", "image-generation"],
},
{
id: "stable-diffusion",
name: "Stable Diffusion XL",
+ avatar: "stablediffusion.jpeg",
free: false,
auth: true,
high_context: false,
+ default: false,
tag: ["open-source", "unstable", "image-generation"],
},
{
id: "dall-e-2",
name: "DALLE 2",
+ avatar: "dalle.jpeg",
free: true,
auth: true,
high_context: false,
+ default: true,
tag: ["free", "official", "image-generation"],
},
{
id: "dall-e-3",
name: "DALLE 3",
+ avatar: "dalle.jpeg",
free: false,
auth: true,
high_context: false,
+ default: true,
tag: ["official", "image-generation"],
},
{
id: "gpt-4-32k-0613",
name: "GPT-4-32k",
+ avatar: "gpt432k.webp",
free: false,
auth: true,
high_context: true,
+ default: false,
tag: ["official", "high-quality", "high-price"],
},
]);
-export const defaultModels = [
- "gpt-3.5-turbo-0613",
- "gpt-3.5-turbo-16k-0613",
- "gpt-4-0613",
- "gpt-4-1106-preview",
-
- "gpt-4-v",
- "gpt-4-dalle",
-
- "azure-gpt-3.5-turbo",
- "azure-gpt-3.5-turbo-16k",
- "azure-gpt-4",
- "azure-gpt-4-1106-preview",
- "azure-gpt-4-vision-preview",
- "azure-gpt-4-32k",
-
- "claude-1-100k",
- "claude-2",
- "claude-2.1",
-
- "spark-desk-v3",
- "qwen-plus",
- "hunyuan",
- "zhipu-chatglm-turbo",
- "baichuan-53b",
-
- "gemini-pro",
- "gemini-pro-vision",
-
- "dall-e-2",
- "midjourney-fast",
- "stable-diffusion",
-];
-
export let allModels: string[] = supportModels.map((model) => model.id);
export const planModels: PlanModel[] = [
@@ -485,58 +548,6 @@ export const planModels: PlanModel[] = [
{ id: "midjourney-fast", level: 1 },
];
-export const modelAvatars: Record
= {
- "gpt-3.5-turbo-0613": "gpt35turbo.png",
- "gpt-3.5-turbo-16k-0613": "gpt35turbo16k.webp",
- "gpt-3.5-turbo-1106": "gpt35turbo16k.webp",
- "gpt-3.5-turbo-fast": "gpt35turbo16k.webp",
- "gpt-3.5-turbo-16k-fast": "gpt35turbo16k.webp",
- "gpt-4-0613": "gpt4.png",
- "gpt-4-1106-preview": "gpt432k.webp",
- "gpt-4-vision-preview": "gpt4v.png",
- "gpt-4-all": "gpt4.png",
- "gpt-4-32k-0613": "gpt432k.webp",
- "gpt-4-v": "gpt4v.png",
- "gpt-4-dalle": "gpt4dalle.png",
- "azure-gpt-3.5-turbo": "gpt35turbo.png",
- "azure-gpt-3.5-turbo-16k": "gpt35turbo16k.webp",
- "azure-gpt-4": "gpt4.png",
- "azure-gpt-4-1106-preview": "gpt432k.webp",
- "azure-gpt-4-vision-preview": "gpt4v.png",
- "azure-gpt-4-32k": "gpt432k.webp",
- "claude-1-100k": "claude.png",
- "claude-2": "claude100k.png",
- "claude-2.1": "claude100k.png",
- "stable-diffusion": "stablediffusion.jpeg",
- "llama-2-70b": "llama2.webp",
- "llama-2-13b": "llama2.webp",
- "llama-2-7b": "llama2.webp",
- "code-llama-34b": "llamacode.webp",
- "code-llama-13b": "llamacode.webp",
- "code-llama-7b": "llamacode.webp",
- "dall-e-3": "dalle.jpeg",
- "dall-e-2": "dalle.jpeg",
- midjourney: "midjourney.jpg",
- "midjourney-fast": "midjourney.jpg",
- "midjourney-turbo": "midjourney.jpg",
- "bing-creative": "newbing.jpg",
- "chat-bison-001": "palm2.webp",
- "gemini-pro": "gemini.jpeg",
- "gemini-pro-vision": "gemini.jpeg",
- "zhipu-chatglm-turbo": "chatglm.png",
- "qwen-plus-net": "tongyi.png",
- "qwen-plus": "tongyi.png",
- "qwen-turbo-net": "tongyi.png",
- "qwen-turbo": "tongyi.png",
- "spark-desk-v3": "sparkdesk.jpg",
- "spark-desk-v2": "sparkdesk.jpg",
- "spark-desk-v1.5": "sparkdesk.jpg",
- hunyuan: "hunyuan.png",
- "360-gpt-v9": "360gpt.png",
- "baichuan-53b": "baichuan.png",
- "skylark-chat": "skylark.jpg",
-};
-
export const subscriptionPrize: Record = {
1: 42,
2: 76,
diff --git a/app/src/resources/i18n/cn.json b/app/src/resources/i18n/cn.json
index 73e87c66..c5e67263 100644
--- a/app/src/resources/i18n/cn.json
+++ b/app/src/resources/i18n/cn.json
@@ -31,6 +31,7 @@
"true": "是",
"false": "否",
"unknown": "未知",
+ "update": "更新",
"scroll-down": "滚至最新",
"broadcast": "公告",
"fatal": "应用崩溃",
@@ -400,6 +401,25 @@
"generate": "批量生成",
"generate-result": "生成结果",
"error": "请求失败",
+ "market": {
+ "title": "模型市场",
+ "model-name": "模型名称",
+ "model-name-placeholder": "请输入模型昵称 (如:GPT-4)",
+ "model-id": "模型 ID",
+ "model-id-placeholder": "请输入模型 ID (如:gpt-4-0613)",
+ "model-description": "模型简介",
+ "model-description-placeholder": "请输入模型简介",
+ "model-context": "高上下文",
+ "model-context-tip": "模型是否为高上下文模型(高上下文模型文件解析时不会被长内容截断)",
+ "model-is-default": "默认模型",
+ "model-is-default-tip": "模型是否添加至默认模型列表(未添加至默认模型列表的模型默认不会出现在首页模型列表中)",
+ "model-tag": "模型标签",
+ "model-image": "模型图片",
+ "update-success": "更新成功",
+ "update-success-prompt": "模型市场设置已成功提交更新至服务器。",
+ "update-failed": "更新失败",
+ "update-failed-prompt": "更新请求失败,原因:{{reason}}"
+ },
"redeem": {
"quota": "点数",
"used": "已用个数",
diff --git a/app/src/resources/i18n/en.json b/app/src/resources/i18n/en.json
index f9f07d79..612a00f7 100644
--- a/app/src/resources/i18n/en.json
+++ b/app/src/resources/i18n/en.json
@@ -445,6 +445,25 @@
"quota": "Number of points",
"used": "Used Count",
"total": "Total"
+ },
+ "market": {
+ "title": "Model Market",
+ "model-name": "model name",
+ "model-name-placeholder": "Please enter the model nickname (e.g. GPT-4)",
+ "model-id": "Former ID",
+ "model-id-placeholder": "Please enter the model ID (e.g. gpt-4-0613)",
+ "model-description": "Introduction to the Model",
+ "model-description-placeholder": "Please enter a model introduction",
+ "model-context": "High Context",
+ "model-context-tip": "Whether the model is a high context model (high context model files are not truncated by long content when parsed)",
+ "model-is-default": "Default Model",
+ "model-is-default-tip": "Whether the model is added to the default model list (models not added to the default model list will not appear in the home model list by default)",
+ "model-tag": "Model label",
+ "update-success": "Upgrade successful",
+ "update-success-prompt": "Model Marketplace settings were successfully submitted for update to the server.",
+ "update-failed": "Update failed",
+ "update-failed-prompt": "Update request failed for {{reason}}",
+ "model-image": "Model Picture"
}
},
"mask": {
@@ -490,5 +509,6 @@
"register-success-prompt": "You have successfully registered, welcome!"
},
"reset": "Reset",
- "request-error": "Request failed for {{reason}}"
+ "request-error": "Request failed for {{reason}}",
+ "update": "Updated"
}
\ No newline at end of file
diff --git a/app/src/resources/i18n/ja.json b/app/src/resources/i18n/ja.json
index f3a88b98..9802432c 100644
--- a/app/src/resources/i18n/ja.json
+++ b/app/src/resources/i18n/ja.json
@@ -445,6 +445,25 @@
"quota": "Whirlies",
"used": "使用数",
"total": "合計"
+ },
+ "market": {
+ "title": "モデルマーケット",
+ "model-name": "モデル名",
+ "model-name-placeholder": "モデルのニックネームを入力してください(例: GPT -4 )",
+ "model-id": "モデルID",
+ "model-id-placeholder": "モデルIDを入力してください(例: gpt -4 -0613 )",
+ "model-description": "モデルの紹介",
+ "model-description-placeholder": "モデル紹介を入力してください",
+ "model-context": "ハイコンテキスト",
+ "model-context-tip": "モデルがハイコンテキストモデルであるかどうか(ハイコンテキストモデルファイルは、解析時に長いコンテンツによって切り捨てられません)",
+ "model-is-default": "デフォルトモデル",
+ "model-is-default-tip": "モデルがデフォルトモデルリストに追加されるかどうか(デフォルトでは、デフォルトモデルリストに追加されていないモデルはホームモデルリストに表示されません)",
+ "model-tag": "モデルラベル",
+ "update-success": "正常に更新されました",
+ "update-success-prompt": "モデルマーケットプレイスの設定は、サーバーへの更新のために正常に送信されました。",
+ "update-failed": "更新に失敗",
+ "update-failed-prompt": "{{reason}}の更新リクエストが失敗しました",
+ "model-image": "モデル写真"
}
},
"mask": {
@@ -490,5 +509,6 @@
"register-success-prompt": "登録が完了しました。ようこそ!"
},
"reset": "リセット",
- "request-error": "{{reason}}のためにリクエストできませんでした"
+ "request-error": "{{reason}}のためにリクエストできませんでした",
+ "update": "更新"
}
\ No newline at end of file
diff --git a/app/src/resources/i18n/ru.json b/app/src/resources/i18n/ru.json
index e6e401eb..f4740057 100644
--- a/app/src/resources/i18n/ru.json
+++ b/app/src/resources/i18n/ru.json
@@ -445,6 +445,25 @@
"quota": "Вихри",
"used": "Количество использованных",
"total": "Итого"
+ },
+ "market": {
+ "title": "Модельный рынок",
+ "model-name": "Модели засоренности",
+ "model-name-placeholder": "Введите псевдоним модели (например, GPT-4)",
+ "model-id": "Идентификатор модели",
+ "model-id-placeholder": "Введите идентификатор модели (например, gpt-4-0613)",
+ "model-description": "Введение в модель",
+ "model-description-placeholder": "Пожалуйста, введите введение в модель",
+ "model-context": "Высокий контекст",
+ "model-context-tip": "Является ли модель высококонтекстной моделью (файлы высококонтекстной модели не усекаются длинным содержимым при синтаксическом анализе)",
+ "model-is-default": "Модель по умолчанию",
+ "model-is-default-tip": "Добавлена ли модель в список моделей по умолчанию (модели, не добавленные в список моделей по умолчанию, не будут отображаться в списке моделей дома по умолчанию)",
+ "model-tag": "Этикетка модели",
+ "update-success": "Успешно обновлено",
+ "update-success-prompt": "Настройки Model Marketplace успешно отправлены на обновление на сервер.",
+ "update-failed": "Ошибка обновления",
+ "update-failed-prompt": "Запрос на обновление не выполнен по {{reason}}",
+ "model-image": "Изображение модели"
}
},
"mask": {
@@ -490,5 +509,6 @@
"register-success-prompt": "Вы успешно зарегистрировались, добро пожаловать!"
},
"reset": "сброс",
- "request-error": "Запрос не выполнен по {{reason}}"
+ "request-error": "Запрос не выполнен по {{reason}}",
+ "update": "Обновить"
}
\ No newline at end of file
diff --git a/app/src/router.tsx b/app/src/router.tsx
index e695b433..3f316f41 100644
--- a/app/src/router.tsx
+++ b/app/src/router.tsx
@@ -13,6 +13,7 @@ const Article = lazy(() => import("@/routes/Article.tsx"));
const Admin = lazy(() => import("@/routes/Admin.tsx"));
const Dashboard = lazy(() => import("@/routes/admin/DashBoard.tsx"));
+const Market = lazy(() => import("@/routes/admin/Market.tsx"));
const Channel = lazy(() => import("@/routes/admin/Channel.tsx"));
const System = lazy(() => import("@/routes/admin/System.tsx"));
const Charge = lazy(() => import("@/routes/admin/Charge.tsx"));
@@ -104,6 +105,15 @@ const router = createBrowserRouter(
),
},
+ {
+ id: "admin-market",
+ path: "market",
+ element: (
+
+
+
+ ),
+ },
{
id: "admin-channel",
path: "channel",
diff --git a/app/src/routes/admin/Market.tsx b/app/src/routes/admin/Market.tsx
new file mode 100644
index 00000000..2c22a254
--- /dev/null
+++ b/app/src/routes/admin/Market.tsx
@@ -0,0 +1,459 @@
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card.tsx";
+import { useTranslation } from "react-i18next";
+import { Dispatch, useEffect, useMemo, useReducer, useRef } from "react";
+import { Model as RawModel } from "@/api/types.ts";
+import { supportModels } from "@/conf.ts";
+import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
+import { Input } from "@/components/ui/input.tsx";
+import { GripVertical } from "lucide-react";
+import { generateRandomChar } from "@/utils/base.ts";
+import Require from "@/components/Require.tsx";
+import { Textarea } from "@/components/ui/textarea.tsx";
+import { toast } from "sonner";
+import Tips from "@/components/Tips.tsx";
+import { Switch } from "@/components/ui/switch.tsx";
+import { Toggle } from "@/components/ui/toggle.tsx";
+import { marketEditableTags, modelImages } from "@/admin/market.ts";
+
+type Model = RawModel & {
+ seed?: string;
+};
+
+type MarketForm = Model[];
+
+const initialState: MarketForm = [];
+
+const generateSeed = () => generateRandomChar(8);
+
+function reducer(state: MarketForm, action: any): MarketForm {
+ switch (action.type) {
+ case "set":
+ return [
+ ...action.payload.map((model: RawModel) => ({
+ ...model,
+ seed: generateSeed(),
+ })),
+ ];
+ case "add":
+ return [
+ ...state,
+ {
+ ...action.payload,
+ seed: generateSeed(),
+ },
+ ];
+ case "remove":
+ let { idx } = action.payload;
+ return [...state.slice(0, idx), ...state.slice(idx + 1)];
+ case "update":
+ let { index, data } = action.payload;
+ return [...state.slice(0, index), data, ...state.slice(index + 1)];
+ case "update-id":
+ return [
+ ...state.map((model, idx) => {
+ if (idx === action.payload.idx) {
+ return { ...model, id: action.payload.id };
+ }
+ return model;
+ }),
+ ];
+ case "update-name":
+ return [
+ ...state.map((model, idx) => {
+ if (idx === action.payload.idx) {
+ return { ...model, name: action.payload.name };
+ }
+ return model;
+ }),
+ ];
+ case "update-description":
+ return [
+ ...state.map((model, idx) => {
+ if (idx === action.payload.idx) {
+ return { ...model, description: action.payload.description };
+ }
+ return model;
+ }),
+ ];
+ case "update-context":
+ return [
+ ...state.map((model, idx) => {
+ if (idx === action.payload.idx) {
+ return { ...model, high_context: action.payload.context };
+ }
+ return model;
+ }),
+ ];
+ case "update-default":
+ return [
+ ...state.map((model, idx) => {
+ if (idx === action.payload.idx) {
+ return { ...model, default: action.payload.default };
+ }
+ return model;
+ }),
+ ];
+ case "update-tags":
+ return [
+ ...state.map((model, idx) => {
+ if (idx === action.payload.idx) {
+ return { ...model, tag: action.payload.tags };
+ }
+ return model;
+ }),
+ ];
+ case "add-tag":
+ return [
+ ...state.map((model, idx) => {
+ if (idx === action.payload.idx) {
+ const tag = model.tag || [];
+ tag.push(action.payload.tag);
+ return {
+ ...model,
+ tag: [...tag],
+ };
+ }
+ return model;
+ }),
+ ];
+ case "remove-tag":
+ return [
+ ...state.map((model, idx) => {
+ if (idx === action.payload.idx) {
+ const tag = model.tag || [];
+ return {
+ ...model,
+ tag: tag.filter((t) => t !== action.payload.tag),
+ };
+ }
+ return model;
+ }),
+ ];
+ case "set-avatar":
+ return [
+ ...state.map((model, idx) => {
+ if (idx === action.payload.idx) {
+ return { ...model, avatar: action.payload.avatar };
+ }
+ return model;
+ }),
+ ];
+ case "replace":
+ const { from, to } = action.payload;
+ const [removed] = state.splice(from, 1);
+ state.splice(to, 0, removed);
+ return [...state];
+ default:
+ throw new Error();
+ }
+}
+
+type MarketTagsProps = {
+ tag: string[] | undefined;
+ idx: number;
+ dispatch: Dispatch;
+};
+
+function MarketTags({ tag, idx, dispatch }: MarketTagsProps) {
+ const { t } = useTranslation();
+ const tags = useMemo((): Record => {
+ const selected = tag || [];
+
+ return marketEditableTags.reduce(
+ (acc, name) => {
+ acc[name] = selected.includes(name);
+ return acc;
+ },
+ {} as Record,
+ );
+ }, [tag]);
+
+ return (
+
+ {tags &&
+ Object.keys(tags).map((name) => (
+ {
+ dispatch({
+ type: state ? "add-tag" : "remove-tag",
+ payload: {
+ idx,
+ tag: name,
+ },
+ });
+ }}
+ >
+ {t(`tag.${name}`)}
+
+ ))}
+
+ );
+}
+
+type MarketImageProps = {
+ image: string;
+ idx: number;
+ dispatch: Dispatch;
+};
+
+function MarketImage({ image, idx, dispatch }: MarketImageProps) {
+ const { t } = useTranslation();
+
+ return (
+
+ {modelImages.map((source) => (
+
{
+ if (!state) return;
+ dispatch({
+ type: "set-avatar",
+ payload: {
+ idx,
+ avatar: source,
+ },
+ });
+ }}
+ >
+
+
+ ))}
+
+ );
+}
+
+function Market() {
+ const { t } = useTranslation();
+ const [form, dispatch] = useReducer(reducer, initialState);
+ const timer = useRef(null);
+ const sync = useRef(false);
+
+ useEffect(() => {
+ if (form.length === 0 && supportModels.length > 0) {
+ dispatch({ type: "set", payload: [...supportModels] });
+ }
+ sync.current = true;
+ }, [supportModels]);
+
+ useEffect(() => {
+ if (timer.current) {
+ clearTimeout(timer.current);
+ }
+
+ timer.current = Number(
+ setTimeout(() => {
+ if (sync.current) {
+ sync.current = false;
+ return;
+ }
+
+ console.debug(
+ `[market] model market migrated, sync to server (models: ${form.length})`,
+ );
+
+ toast(t("admin.market.update-success"), {
+ description: t("admin.market.update-success-prompt"),
+ });
+ }, 2000),
+ );
+ console.debug(
+ `[market] model market changed, wait for sync... (triggered task id: ${timer.current})`,
+ );
+ }, [form]);
+
+ return (
+
+
+
+ {t("admin.market.title")}
+
+
+ {
+ const { destination, source } = result;
+ if (
+ !destination ||
+ destination.index === source.index ||
+ destination.index === -1
+ )
+ return;
+
+ const from = source.index;
+ const to = destination.index;
+
+ dispatch({ type: "replace", payload: { from, to } });
+ }}
+ >
+
+ {(provided) => (
+
+ {form.map((model, index) => (
+
+ {(provided) => (
+
+
+
+
+
+
+ {t("admin.market.model-name")}
+
+ {
+ dispatch({
+ type: "update-name",
+ payload: {
+ idx: index,
+ name: e.target.value,
+ },
+ });
+ }}
+ />
+
+
+
+
+ {t("admin.market.model-id")}
+
+ {
+ dispatch({
+ type: "update-id",
+ payload: {
+ idx: index,
+ id: e.target.value,
+ },
+ });
+ }}
+ />
+
+
+ {t("admin.market.model-description")}
+
+
+
+ {t("admin.market.model-context")}
+
+
+ {
+ dispatch({
+ type: "update-context",
+ payload: {
+ idx: index,
+ context: state,
+ },
+ });
+ }}
+ />
+
+
+
+ {t("admin.market.model-is-default")}
+
+
+ {
+ dispatch({
+ type: "update-default",
+ payload: {
+ idx: index,
+ default: state,
+ },
+ });
+ }}
+ />
+
+
+ {t("admin.market.model-tag")}
+
+
+
+ {t("admin.market.model-image")}
+
+
+
+
+ )}
+
+ ))}
+ {provided.placeholder}
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default Market;
diff --git a/app/src/store/chat.ts b/app/src/store/chat.ts
index b569ac8e..9a43ecd0 100644
--- a/app/src/store/chat.ts
+++ b/app/src/store/chat.ts
@@ -3,7 +3,7 @@ import { ConversationInstance, Model } from "@/api/types.ts";
import { Message } from "@/api/types.ts";
import { insertStart } from "@/utils/base.ts";
import { RootState } from "./index.ts";
-import { defaultModels, planModels, supportModels } from "@/conf.ts";
+import { planModels, supportModels } from "@/conf.ts";
import { getBooleanMemory, getMemory, setMemory } from "@/utils/memory.ts";
type initialStateType = {
@@ -42,7 +42,9 @@ export function getModelList(
models && models.length
? models.split(",").filter((item) => inModel(item))
: [];
- const target = list.length ? list : defaultModels;
+ const target = list.length
+ ? list
+ : supportModels.filter((item) => item.default).map((item) => item.id);
const selection = getModel(select);
if (!target.includes(selection)) target.push(selection);
return target;
diff --git a/app/src/types/ui.d.ts b/app/src/types/ui.d.ts
index b5382cda..34105aa1 100644
--- a/app/src/types/ui.d.ts
+++ b/app/src/types/ui.d.ts
@@ -1 +1,9 @@
declare module "@radix-ui/react-select-area";
+
+declare module "sonner" {
+ export interface ToastProps {
+ description: string;
+ }
+
+ export function toast(title: string, content?: ToastProps): void;
+}
diff --git a/app/src/utils/base.ts b/app/src/utils/base.ts
index a4bd036d..f5e53c04 100644
--- a/app/src/utils/base.ts
+++ b/app/src/utils/base.ts
@@ -52,3 +52,11 @@ export function getErrorMessage(error: any): string {
export function isAsyncFunc(fn: any): boolean {
return fn.constructor.name === "AsyncFunction";
}
+
+export function generateRandomChar(n: number): string {
+ const chars = "abcdefghijklmnopqrstuvwxyz";
+ return Array(n)
+ .fill(0)
+ .map(() => chars[Math.floor(Math.random() * chars.length)])
+ .join("");
+}