diff --git a/adapter/oneapi/handler.go b/adapter/oneapi/handler.go index 480628a..6e0a929 100644 --- a/adapter/oneapi/handler.go +++ b/adapter/oneapi/handler.go @@ -14,7 +14,7 @@ type AdapterProps struct { } func HandleRequest(props *AdapterProps, hook globals.Hook) error { - instance := NewChatInstanceFromConfig(props.Model) + instance := NewChatInstanceFromConfig() return instance.CreateStreamChatRequest(&ChatProps{ Model: instance.FormatModel(props.Model), Message: instance.FormatMessage(props.Message), diff --git a/adapter/oneapi/struct.go b/adapter/oneapi/struct.go index 5f6f14b..7038c51 100644 --- a/adapter/oneapi/struct.go +++ b/adapter/oneapi/struct.go @@ -1,7 +1,6 @@ package oneapi import ( - "chat/globals" "chat/utils" "fmt" "github.com/spf13/viper" @@ -39,14 +38,7 @@ func NewChatInstance(endpoint, apiKey string) *ChatInstance { } } -func NewChatInstanceFromConfig(model string) *ChatInstance { - if model == globals.Claude2100k { - return NewChatInstance( - viper.GetString("oneapi.claude.endpoint"), - viper.GetString("oneapi.claude.apikey"), - ) - } - +func NewChatInstanceFromConfig() *ChatInstance { return NewChatInstance( viper.GetString("oneapi.endpoint"), viper.GetString("oneapi.apikey"), diff --git a/app/package.json b/app/package.json index 5d0a173..91d19e7 100644 --- a/app/package.json +++ b/app/package.json @@ -44,6 +44,7 @@ "react-router-dom": "^6.17.0", "react-syntax-highlighter": "^15.5.0", "rehype-katex": "^6.0.3", + "remark-breaks": "^4.0.0", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", "sort-by": "^1.2.0", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index beef201..00ff129 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -104,6 +104,9 @@ dependencies: rehype-katex: specifier: ^6.0.3 version: 6.0.3 + remark-breaks: + specifier: ^4.0.0 + version: 4.0.0 remark-gfm: specifier: ^3.0.1 version: 3.0.1 @@ -2853,6 +2856,12 @@ packages: '@types/unist': 2.0.9 dev: false + /@types/mdast@4.0.3: + resolution: {integrity: sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==} + dependencies: + '@types/unist': 2.0.9 + dev: false + /@types/ms@0.7.33: resolution: {integrity: sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ==} dev: false @@ -2904,6 +2913,10 @@ packages: resolution: {integrity: sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==} dev: false + /@types/unist@3.0.2: + resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + dev: false + /@types/use-sync-external-store@0.0.3: resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} dev: false @@ -3719,6 +3732,12 @@ packages: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} dev: false + /devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dependencies: + dequal: 2.0.3 + dev: false + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -5068,6 +5087,15 @@ packages: unist-util-visit-parents: 5.1.3 dev: false + /mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + dependencies: + '@types/mdast': 4.0.3 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + dev: false + /mdast-util-from-markdown@1.3.1: resolution: {integrity: sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==} dependencies: @@ -5151,6 +5179,13 @@ packages: mdast-util-to-markdown: 1.5.0 dev: false + /mdast-util-newline-to-break@2.0.0: + resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} + dependencies: + '@types/mdast': 4.0.3 + mdast-util-find-and-replace: 3.0.1 + dev: false + /mdast-util-phrasing@3.0.1: resolution: {integrity: sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==} dependencies: @@ -6166,6 +6201,14 @@ packages: engines: {node: '>= 0.10'} dev: true + /remark-breaks@4.0.0: + resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} + dependencies: + '@types/mdast': 4.0.3 + mdast-util-newline-to-break: 2.0.0 + unified: 11.0.4 + dev: false + /remark-gfm@3.0.1: resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==} dependencies: @@ -6820,6 +6863,18 @@ packages: vfile: 5.3.7 dev: false + /unified@11.0.4: + resolution: {integrity: sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==} + dependencies: + '@types/unist': 3.0.2 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.1.0 + vfile: 6.0.1 + dev: false + /unique-string@2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -6844,6 +6899,12 @@ packages: '@types/unist': 2.0.9 dev: false + /unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + dependencies: + '@types/unist': 3.0.2 + dev: false + /unist-util-position@4.0.4: resolution: {integrity: sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg==} dependencies: @@ -6863,6 +6924,12 @@ packages: '@types/unist': 2.0.9 dev: false + /unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + dependencies: + '@types/unist': 3.0.2 + dev: false + /unist-util-visit-parents@5.1.3: resolution: {integrity: sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==} dependencies: @@ -6870,6 +6937,13 @@ packages: unist-util-is: 5.2.1 dev: false + /unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-is: 6.0.0 + dev: false + /unist-util-visit@4.1.2: resolution: {integrity: sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==} dependencies: @@ -6972,6 +7046,13 @@ packages: unist-util-stringify-position: 3.0.3 dev: false + /vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + dev: false + /vfile@5.3.7: resolution: {integrity: sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==} dependencies: @@ -6981,6 +7062,14 @@ packages: vfile-message: 3.1.4 dev: false + /vfile@6.0.1: + resolution: {integrity: sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==} + dependencies: + '@types/unist': 3.0.2 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + dev: false + /vite-plugin-html@3.2.0(vite@4.5.0): resolution: {integrity: sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==} peerDependencies: diff --git a/app/public/icons/chatglm.png b/app/public/icons/chatglm.png new file mode 100644 index 0000000..98a294a Binary files /dev/null and b/app/public/icons/chatglm.png differ diff --git a/app/public/icons/claude.png b/app/public/icons/claude.png new file mode 100644 index 0000000..f7d90e3 Binary files /dev/null and b/app/public/icons/claude.png differ diff --git a/app/public/icons/claude100k.png b/app/public/icons/claude100k.png new file mode 100644 index 0000000..c58537d Binary files /dev/null and b/app/public/icons/claude100k.png differ diff --git a/app/public/icons/dalle.jpeg b/app/public/icons/dalle.jpeg new file mode 100644 index 0000000..90b95dc Binary files /dev/null and b/app/public/icons/dalle.jpeg differ diff --git a/app/public/icons/gpt35turbo.png b/app/public/icons/gpt35turbo.png new file mode 100644 index 0000000..e2f9598 Binary files /dev/null and b/app/public/icons/gpt35turbo.png differ diff --git a/app/public/icons/gpt35turbo16k.webp b/app/public/icons/gpt35turbo16k.webp new file mode 100644 index 0000000..058f399 Binary files /dev/null and b/app/public/icons/gpt35turbo16k.webp differ diff --git a/app/public/icons/gpt4.png b/app/public/icons/gpt4.png new file mode 100644 index 0000000..4fffc97 Binary files /dev/null and b/app/public/icons/gpt4.png differ diff --git a/app/public/icons/gpt432k.webp b/app/public/icons/gpt432k.webp new file mode 100644 index 0000000..07ad294 Binary files /dev/null and b/app/public/icons/gpt432k.webp differ diff --git a/app/public/icons/gpt4dalle.png b/app/public/icons/gpt4dalle.png new file mode 100644 index 0000000..51975ea Binary files /dev/null and b/app/public/icons/gpt4dalle.png differ diff --git a/app/public/icons/gpt4v.png b/app/public/icons/gpt4v.png new file mode 100644 index 0000000..24a0a5e Binary files /dev/null and b/app/public/icons/gpt4v.png differ diff --git a/app/public/icons/hunyuan.png b/app/public/icons/hunyuan.png new file mode 100644 index 0000000..9c550d2 Binary files /dev/null and b/app/public/icons/hunyuan.png differ diff --git a/app/public/icons/llama2.webp b/app/public/icons/llama2.webp new file mode 100644 index 0000000..93f052f Binary files /dev/null and b/app/public/icons/llama2.webp differ diff --git a/app/public/icons/llamacode.webp b/app/public/icons/llamacode.webp new file mode 100644 index 0000000..f0af680 Binary files /dev/null and b/app/public/icons/llamacode.webp differ diff --git a/app/public/icons/midjourney.jpg b/app/public/icons/midjourney.jpg new file mode 100644 index 0000000..0812cbd Binary files /dev/null and b/app/public/icons/midjourney.jpg differ diff --git a/app/public/icons/newbing.jpg b/app/public/icons/newbing.jpg new file mode 100644 index 0000000..85a79c8 Binary files /dev/null and b/app/public/icons/newbing.jpg differ diff --git a/app/public/icons/palm2.webp b/app/public/icons/palm2.webp new file mode 100644 index 0000000..1e170b3 Binary files /dev/null and b/app/public/icons/palm2.webp differ diff --git a/app/public/icons/sparkdesk.jpg b/app/public/icons/sparkdesk.jpg new file mode 100644 index 0000000..560985a Binary files /dev/null and b/app/public/icons/sparkdesk.jpg differ diff --git a/app/public/icons/stablediffusion.jpeg b/app/public/icons/stablediffusion.jpeg new file mode 100644 index 0000000..2d1a2d4 Binary files /dev/null and b/app/public/icons/stablediffusion.jpeg differ diff --git a/app/public/icons/tongyi.png b/app/public/icons/tongyi.png new file mode 100644 index 0000000..dc0c159 Binary files /dev/null and b/app/public/icons/tongyi.png differ diff --git a/app/src/assets/globals.less b/app/src/assets/globals.less index 7d63053..81181dd 100644 --- a/app/src/assets/globals.less +++ b/app/src/assets/globals.less @@ -50,6 +50,8 @@ --chat-bot-background: rgba(88, 166, 255, .04); --chat-bot-border: rgba(88, 166, 255, .12); --chat-bot-border-hover: rgba(88, 166, 255, .24); + + --model-card-background: rgba(222,214,200,.2); } .dark { @@ -97,6 +99,8 @@ --chat-bot-background: rgba(88, 166, 255, .1); --chat-bot-border: rgba(88, 166, 255, .2); --chat-bot-border-hover: rgba(88, 166, 255, .25); + + --model-card-background: rgba(152,153,165,.05); } } diff --git a/app/src/assets/pages/home.less b/app/src/assets/pages/home.less index 425515f..e4a350e 100644 --- a/app/src/assets/pages/home.less +++ b/app/src/assets/pages/home.less @@ -8,6 +8,237 @@ background: hsl(var(--background-container)); } +.model-market { + display: flex; + flex-direction: column; + width: 100%; + padding: 1rem 3.5rem; + height: max-content; + max-height: calc(100vh - 56px); + overflow: auto; + scrollbar-width: thin; + + @media (max-width: 768px) { + padding: 1rem; + } + + .title { + font-size: 24px; + } + + & > * { + flex-shrink: 0; + } + + .search-bar { + position: relative; + margin: 1rem auto; + width: 100%; + + .search-icon { + position: absolute; + top: 50%; + left: 1rem; + transform: translateY(-50%); + width: 1rem; + height: 1rem; + } + + .clear-icon { + position: absolute; + top: 50%; + right: 1rem; + transform: translateY(-50%); + width: 1rem; + height: 1rem; + opacity: 0; + transition: 0.25s; + cursor: pointer; + + &.active { + opacity: .8; + + &:hover { + opacity: 1; + } + } + } + + .input-box { + padding: 0 2.5rem; + } + } + + .model-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; + } + + .model-item { + position: relative; + display: flex; + flex-direction: row; + align-items: center; + user-select: none; + flex-wrap: wrap; + padding: 1rem 1.5rem; + margin: 0.5rem; + border: 1px solid hsl(var(--border-hover)); + border-radius: var(--radius); + transition: 0.25s; + cursor: pointer; + animation: fadein 0.25s forwards ease-in-out; + opacity: 0; + width: 100%; + + @media (min-width: 960px) { + width: calc(50% - 1rem); + } + + @keyframes fadein { + from { opacity: 0; transform: translateY(2.5rem); } + to { opacity: 1; transform: translateY(0); } + } + + &:hover { + border-color: hsl(var(--border-active)); + padding: 1rem 1.5rem 1rem 2rem; + + &:before { + width: 100%; + } + } + + &.active { + border-color: hsl(var(--border-active)); + + &:before { + width: 100%; + } + } + + &:before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 0; + height: 100%; + background: var(--model-card-background); + transition: 0.5s; + z-index: -1; + border-radius: var(--radius); + } + + .model-avatar { + border-radius: var(--radius); + width: 3rem; + height: 3rem; + margin-right: 1rem; + } + + .model-action { + button { + background: none !important; + } + + @keyframes rotate { + 0% { + transform: rotate(0deg); + } + 25% { + transform: rotate(25deg); + } + 50% { + transform: rotate(0deg); + } + 75% { + transform: rotate(-25deg); + } + 100% { + transform: rotate(0deg); + } + } + + svg { + animation: rotate 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); + } + } + + .model-name { + &.pro { + // gold color gradient + background: linear-gradient(to right, hsl(45, 100%, 70%) 0%, hsl(46, 100%, 58%) 50%, hsl(46, 100%, 50%) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + } + + .model-tag { + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin-top: 0.25rem; + transform: translateY(0.25rem); + + & > .tag-item { + flex-shrink: 0; + margin-right: 0.25rem; + padding: 0.125rem 0.45rem; + background: hsl(var(--input)); + border-radius: 4px; + font-size: 12px; + margin-bottom: 0.25rem; + + &:last-child { + margin-right: 0; + } + } + } + } + + .market-header { + position: relative; + width: 100%; + height: max-content; + + .close-action { + position: absolute; + } + } + + .market-footer { + margin-top: 3rem; + margin-bottom: .5rem; + user-select: none; + padding: 0 1rem; + + a { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + font-size: 14px; + color: hsl(var(--text-secondary)); + transition: 0.25s; + width: max-content; + max-width: 100%; + margin: 0 auto; + text-align: center; + + svg { + flex-shrink: 0; + } + + &:hover { + color: hsl(var(--text)); + } + } + } +} + .conversation-name { color: hsl(var(--text)); font-weight: bold !important; @@ -33,12 +264,21 @@ margin: 0; background: var(--background-sidebar); transition: 0.2s ease-in-out; - transition-property: width, background, box-shadow; + transition-property: width, background, box-shadow, border-right, opacity; border-right: 0; + pointer-events: none; + opacity: 0; &.open { width: 260px; border-right: 1px solid hsl(var(--border)); + pointer-events: auto; + opacity: 1; + } + + &.hidden { + width: 0; + border-right: 0; } .sidebar-content { diff --git a/app/src/components/Markdown.tsx b/app/src/components/Markdown.tsx index 74a64e5..c12a0d9 100644 --- a/app/src/components/Markdown.tsx +++ b/app/src/components/Markdown.tsx @@ -3,6 +3,7 @@ import { atomOneDark as style } from "react-syntax-highlighter/dist/esm/styles/h import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; +import remarkBreaks from "remark-breaks"; import rehypeKatex from "rehype-katex"; import { parseFile } from "./plugins/file.tsx"; import "@/assets/markdown/all.less"; @@ -57,7 +58,7 @@ function Markdown({ children, className }: MarkdownProps) { return ( +
+ {props.icon &&
{props.icon}
} {props.value} {props.badge && (
-

- - - {t("pricing")} - -

{cn && (

请您遵守 diff --git a/app/src/components/home/ChatWrapper.tsx b/app/src/components/home/ChatWrapper.tsx index 6340edc..7a3b10b 100644 --- a/app/src/components/home/ChatWrapper.tsx +++ b/app/src/components/home/ChatWrapper.tsx @@ -22,6 +22,20 @@ import SendButton from "@/components/home/assemblies/SendButton.tsx"; import ChatInput from "@/components/home/assemblies/ChatInput.tsx"; import ScrollAction from "@/components/home/assemblies/ScrollAction.tsx"; +type InterfaceProps = { + setInstance: (instance: HTMLElement | null) => void; +}; + +function Interface({ setInstance }: InterfaceProps) { + const messages = useSelector(selectMessages); + + return messages.length > 0 ? ( + + ) : ( + + ); +} + function ChatWrapper() { const { t } = useTranslation(); const { toast } = useToast(); @@ -32,7 +46,6 @@ function ChatWrapper() { const auth = useSelector(selectAuthenticated); const model = useSelector(selectModel); const web = useSelector(selectWeb); - const messages = useSelector(selectMessages); const target = useRef(null); const context = useSelector(contextSelector); const align = useSelector(alignSelector); @@ -114,11 +127,7 @@ function ChatWrapper() { return (

- {messages.length > 0 ? ( - - ) : ( - - )} +
diff --git a/app/src/components/home/ConversationSegment.tsx b/app/src/components/home/ConversationSegment.tsx index a583682..919215d 100644 --- a/app/src/components/home/ConversationSegment.tsx +++ b/app/src/components/home/ConversationSegment.tsx @@ -13,6 +13,7 @@ import { useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; import { ConversationInstance } from "@/conversation/types.ts"; import { useState } from "react"; +import {closeMarket} from "@/store/chat.ts"; type ConversationSegmentProps = { conversation: ConversationInstance; @@ -44,6 +45,7 @@ function ConversationSegment({ return; await toggleConversation(dispatch, conversation.id); if (mobile) dispatch(setMenu(false)); + dispatch(closeMarket()); }} > diff --git a/app/src/components/home/ModelFinder.tsx b/app/src/components/home/ModelFinder.tsx index 80e448a..299cf9e 100644 --- a/app/src/components/home/ModelFinder.tsx +++ b/app/src/components/home/ModelFinder.tsx @@ -6,7 +6,12 @@ import { studentModels, supportModels, } from "@/conf.ts"; -import { selectModel, setModel } from "@/store/chat.ts"; +import { + openMarket, + selectModel, + selectModelList, + setModel, +} from "@/store/chat.ts"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { selectAuthenticated } from "@/store/auth.ts"; @@ -16,6 +21,8 @@ import { modelEvent } from "@/events/model.ts"; import { isSubscribedSelector } from "@/store/subscription.ts"; import { teenagerSelector } from "@/store/package.ts"; import { ToastAction } from "@/components/ui/toast.tsx"; +import { useMemo } from "react"; +import { Sparkles } from "lucide-react"; function GetModel(name: string): Model { return supportModels.find((model) => model.id === name) as Model; @@ -25,6 +32,34 @@ type ModelSelectorProps = { side?: "left" | "right" | "top" | "bottom"; }; +function filterModel(model: Model, subscription: boolean, student: boolean) { + if (subscription && planModels.includes(model.id)) { + return { + name: model.id, + value: model.name, + badge: { variant: "gold", name: "plus" }, + } as SelectItemProps; + } else if (student && studentModels.includes(model.id)) { + return { + name: model.id, + value: model.name, + badge: { variant: "gold", name: "student" }, + } as SelectItemProps; + } else if (expensiveModels.includes(model.id)) { + return { + name: model.id, + value: model.name, + badge: { variant: "gold", name: "expensive" }, + } as SelectItemProps; + } + + return { + name: model.id, + value: model.name, + badge: model.free && { variant: "default", name: "free" }, + } as SelectItemProps; +} + function ModelFinder(props: ModelSelectorProps) { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -34,6 +69,7 @@ function ModelFinder(props: ModelSelectorProps) { const auth = useSelector(selectAuthenticated); const subscription = useSelector(isSubscribedSelector); const student = useSelector(teenagerSelector); + const list = useSelector(selectModelList); modelEvent.bind((target: string) => { if (supportModels.find((m) => m.id === target)) { @@ -43,42 +79,33 @@ function ModelFinder(props: ModelSelectorProps) { } }); - const list = supportModels.map((model: Model): SelectItemProps => { - if (subscription && planModels.includes(model.id)) { - return { - name: model.id, - value: model.name, - badge: { variant: "gold", name: "plus" }, - } as SelectItemProps; - } else if (student && studentModels.includes(model.id)) { - return { - name: model.id, - value: model.name, - badge: { variant: "gold", name: "student" }, - } as SelectItemProps; - } else if (expensiveModels.includes(model.id)) { - return { - name: model.id, - value: model.name, - badge: { variant: "gold", name: "expensive" }, - } as SelectItemProps; - } - - return { - name: model.id, - value: model.name, - badge: model.free && { variant: "default", name: "free" }, - } as SelectItemProps; - }); + const models = useMemo(() => { + const raw = supportModels.filter((model) => list.includes(model.id)); + return [ + ...raw.map( + (model: Model): SelectItemProps => + filterModel(model, subscription, student), + ), + { + icon: , + name: "market", + value: t("market.model"), + }, + ]; + }, [supportModels, subscription, student]); return ( item.name === model) as SelectItemProps} - list={list} + current={models.find((item) => item.name === model) as SelectItemProps} + list={models} maxElements={5} side={props.side} classNameMobile={`model-select-group`} onChange={(value: string) => { + if (value === "market") { + dispatch(openMarket()); + return; + } const model = GetModel(value); console.debug(`[model] select model: ${model.name} (id: ${model.id})`); diff --git a/app/src/components/home/ModelMarket.tsx b/app/src/components/home/ModelMarket.tsx new file mode 100644 index 0000000..b701eb2 --- /dev/null +++ b/app/src/components/home/ModelMarket.tsx @@ -0,0 +1,204 @@ +import { useTranslation } from "react-i18next"; +import { Input } from "@/components/ui/input.tsx"; +import {ChevronLeft, ChevronRight, Link, Plus, Search, Trash2, X} from "lucide-react"; +import React, { useMemo, useState } from "react"; +import {modelAvatars, modelPricingLink, planModels, studentModels, supportModels} from "@/conf.ts"; +import { splitList } from "@/utils/base.ts"; +import { Model } from "@/conversation/types.ts"; +import {useDispatch, useSelector} from "react-redux"; +import {addModelList, closeMarket, removeModelList, selectModel, selectModelList, setModel} from "@/store/chat.ts"; +import {Button} from "@/components/ui/button.tsx"; +import {isSubscribedSelector} from "@/store/subscription.ts"; +import {teenagerSelector} from "@/store/package.ts"; + +type SearchBarProps = { + value: string; + onChange: (value: string) => void; +}; + +function SearchBar({ value, onChange }: SearchBarProps) { + const { t } = useTranslation(); + + return ( +
+ + onChange(e.target.value)} + /> + 0 ? "active" : ""}`} + onClick={() => onChange("")} + /> +
+ ); +} + +type ModelProps = { + model: Model; + className?: string; + style?: React.CSSProperties; +}; + +function ModelItem({ model, className, style }: ModelProps) { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const list = useSelector(selectModelList); + const current = useSelector(selectModel); + + const subscription = useSelector(isSubscribedSelector); + const student = useSelector(teenagerSelector); + + const state = useMemo(() => { + if (current === model.id) return 0; + if (list.includes(model.id)) return 1; + return 2; + }, [model, current, list]); + + const pro = useMemo(() => { + if (subscription && planModels.includes(model.id)) return true; + if (student && studentModels.includes(model.id)) return true; + return false; + }, [model, subscription, student]); + + const avatar = useMemo(() => { + const source = modelAvatars[model.id] || modelAvatars[supportModels[0].id]; + return `/icons/${source}`; + }, [model]); + + return ( +
{ + dispatch(addModelList(model.id)); + dispatch(setModel(model.id)); + dispatch(closeMarket()); + }}> + {model.name} +
+

{model.name}

+
+ {model.tag && + model.tag.map((tag, index) => { + return ( + + {t(`tag.${tag}`)} + + ); + })} +
+
+
+
+ +
+
+ ); +} + +type MarketPlaceProps = { + search: string; +}; + +function MarketPlace({ search }: MarketPlaceProps) { + const { t } = useTranslation(); + const select = useSelector(selectModel); + + const arr = useMemo(() => { + if (search.length === 0) return supportModels; + // fuzzy search + const raw = splitList(search.toLowerCase(), [" ", ",", ";", "-"]); + return supportModels.filter((model) => { + const name = model.name.toLowerCase(); + const tag = (model.tag || []).join(" ").toLowerCase(); + const tag_translated = (model.tag || []) + .map((item) => t(`tag.${item}`)) + .join(" ") + .toLowerCase(); + return raw.every( + (item) => + name.includes(item) || + tag.includes(item) || + tag_translated.includes(item), + ); + }); + }, [search]); + + return ( +
+ {arr.map((model, index) => ( + + ))} +
+ ); +} + +function MarketHeader() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + + return ( +
+ +

+ {t("market.explore")} +

+
+ ) +} + +function MarketFooter() { + const { t } = useTranslation(); + + return ( + + ); +} + +function ModelMarket() { + const [search, setSearch] = useState(""); + + return ( +
+ + + + +
+ ); +} + +export default ModelMarket; diff --git a/app/src/components/home/SideBar.tsx b/app/src/components/home/SideBar.tsx index bab9c49..10b9df3 100644 --- a/app/src/components/home/SideBar.tsx +++ b/app/src/components/home/SideBar.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { selectAuthenticated, selectUsername } from "@/store/auth.ts"; -import { selectCurrent, selectHistory } from "@/store/chat.ts"; +import {closeMarket, selectCurrent, selectHistory} from "@/store/chat.ts"; import { useRef, useState } from "react"; import { ConversationInstance } from "@/conversation/types.ts"; import { useToast } from "@/components/ui/use-toast.ts"; @@ -72,6 +72,7 @@ function SidebarAction({ setOperateConversation }: SidebarActionProps) { onClick={async () => { await toggleConversation(dispatch, -1); if (mobile) dispatch(setMenu(false)); + dispatch(closeMarket()); }} > diff --git a/app/src/conf.ts b/app/src/conf.ts index 32f04a4..2e8241e 100644 --- a/app/src/conf.ts +++ b/app/src/conf.ts @@ -8,7 +8,7 @@ import { } from "@/utils/env.ts"; import { getMemory } from "@/utils/memory.ts"; -export const version = "3.6.23"; +export const version = "3.6.24"; export const dev: boolean = getDev(); export const deploy: boolean = true; export let rest_api: string = getRestApi(deploy); @@ -18,46 +18,173 @@ export const tokenField = getTokenField(deploy); export const supportModels: Model[] = [ // openai models - { id: "gpt-3.5-turbo-0613", name: "GPT-3.5", free: true, auth: false }, - { id: "gpt-3.5-turbo-16k-0613", name: "GPT-3.5-16k", free: true, auth: true }, - { id: "gpt-3.5-turbo-1106", name: "GPT-3.5 1106", free: true, auth: false }, - { id: "gpt-4-0613", name: "GPT-4", free: false, auth: true }, - { id: "gpt-4-1106-preview", name: "GPT-4 Turbo", free: false, auth: true }, + { + id: "gpt-3.5-turbo-0613", + name: "GPT-3.5", + free: true, + auth: false, + tag: ["free", "official"], + }, + { + id: "gpt-3.5-turbo-16k-0613", + name: "GPT-3.5-16k", + free: true, + auth: true, + tag: ["free", "official", "high-context"], + }, + { + id: "gpt-3.5-turbo-1106", + name: "GPT-3.5 1106", + free: true, + auth: false, + tag: ["free", "official"], + }, + { + id: "gpt-4-0613", + name: "GPT-4", + free: false, + auth: true, + tag: ["official", "high-quality"], + }, + { + id: "gpt-4-1106-preview", + name: "GPT-4 Turbo", + free: false, + auth: true, + tag: ["official", "high-context"], + }, // anthropic models - { id: "claude-1-100k", name: "Claude-2", free: true, auth: true }, - { id: "claude-2", name: "Claude-2-100k", free: false, auth: true }, + { + id: "claude-1-100k", + name: "Claude", + free: true, + auth: true, + tag: ["free"], + }, + { + id: "claude-2", + name: "Claude 100k", + free: false, + auth: true, + tag: ["official", "high-context"], + }, // spark desk - { id: "spark-desk-v3", name: "讯飞星火 V3", free: true, auth: true }, - { id: "spark-desk-v2", name: "讯飞星火 V2", free: true, auth: true }, - { id: "spark-desk-v1.5", name: "讯飞星火 V1.5", free: true, auth: true }, + { + id: "spark-desk-v3", + name: "讯飞星火 V3", + free: true, + auth: true, + tag: ["free", "official", "high-quality"], + }, + { + id: "spark-desk-v2", + name: "讯飞星火 V2", + free: true, + auth: true, + tag: ["free", "official"], + }, + { + id: "spark-desk-v1.5", + name: "讯飞星火 V1.5", + free: true, + auth: true, + tag: ["free", "official"], + }, // dashscope models - { id: "qwen-plus-net", name: "通义千问 Plus X", free: false, auth: true }, - { id: "qwen-plus", name: "通义千问 Plus", free: false, auth: true }, - { id: "qwen-turbo-net", name: "通义千问 Turbo X", free: false, auth: true }, - { id: "qwen-turbo", name: "通义千问 Turbo", free: false, auth: true }, + { + id: "qwen-plus-net", + name: "通义千问 Plus Net", + free: false, + auth: true, + tag: ["official", "high-quality", "web"], + }, + { + id: "qwen-plus", + name: "通义千问 Plus", + free: false, + auth: true, + tag: ["official", "high-quality"], + }, + { + id: "qwen-turbo-net", + name: "通义千问 Turbo Net", + free: false, + auth: true, + tag: ["official", "web"], + }, + { + id: "qwen-turbo", + name: "通义千问 Turbo", + free: false, + auth: true, + tag: ["official"], + }, // huyuan models - { id: "hunyuan", name: "腾讯混元 Pro", free: false, auth: true }, + { + id: "hunyuan", + name: "腾讯混元 Pro", + free: false, + auth: true, + tag: ["official"], + }, // zhipu models { id: "zhipu-chatglm-turbo", - name: "ChatGLM Turbo 32k", + name: "ChatGLM Turbo", free: false, auth: true, + tag: ["official", "high-context"], }, // llama models - { id: "llama-2-70b", name: "LLaMa-2 70B", free: false, auth: true }, - { id: "llama-2-13b", name: "LLaMa-2 13B", free: false, auth: true }, - { id: "llama-2-7b", name: "LLaMa-2 7B", free: false, auth: true }, + { + id: "llama-2-70b", + name: "LLaMa-2 70B", + free: false, + auth: true, + tag: ["open-source", "unstable"], + }, + { + id: "llama-2-13b", + name: "LLaMa-2 13B", + free: false, + auth: true, + tag: ["open-source", "unstable"], + }, + { + id: "llama-2-7b", + name: "LLaMa-2 7B", + free: false, + auth: true, + tag: ["open-source", "unstable"], + }, - { id: "code-llama-34b", name: "Code LLaMa 34B", free: false, auth: true }, - { id: "code-llama-13b", name: "Code LLaMa 13B", free: false, auth: true }, - { id: "code-llama-7b", name: "Code LLaMa 7B", free: false, auth: true }, + { + id: "code-llama-34b", + name: "Code LLaMa 34B", + free: false, + auth: true, + tag: ["open-source", "unstable"], + }, + { + id: "code-llama-13b", + name: "Code LLaMa 13B", + free: false, + auth: true, + tag: ["open-source", "unstable"], + }, + { + id: "code-llama-7b", + name: "Code LLaMa 7B", + free: false, + auth: true, + tag: ["open-source", "unstable"], + }, // drawing models { @@ -65,28 +192,110 @@ export const supportModels: Model[] = [ name: "Stable Diffusion XL", free: false, auth: true, + tag: ["open-source", "unstable", "image-generation"], }, // new bing - { id: "bing-creative", name: "New Bing", free: true, auth: true }, + { + id: "bing-creative", + name: "New Bing", + free: true, + auth: true, + tag: ["free", "unstable", "web"], + }, // google palm2 - { id: "chat-bison-001", name: "Palm2", free: true, auth: true }, + { + id: "chat-bison-001", + name: "Google PaLM2", + free: true, + auth: true, + tag: ["free", "english-model"], + }, // dalle models - { id: "dall-e-3", name: "DALLE 3", free: false, auth: true }, - { id: "dall-e-2", name: "DALLE 2", free: true, auth: true }, + { + id: "dall-e-3", + name: "DALLE 3", + free: false, + auth: true, + tag: ["official", "high-price", "image-generation"], + }, + { + id: "dall-e-2", + name: "DALLE 2", + free: true, + auth: true, + tag: ["free", "official", "image-generation"], + }, - { id: "midjourney", name: "Midjourney", free: false, auth: true }, - { id: "midjourney-fast", name: "Midjourney Fast", free: false, auth: true }, - { id: "midjourney-turbo", name: "Midjourney Turbo", free: false, auth: true }, + { + id: "midjourney", + name: "Midjourney Queue", + free: false, + auth: true, + tag: ["official", "image-generation"], + }, + { + id: "midjourney-fast", + name: "Midjourney", + free: false, + auth: true, + tag: ["official", "fast", "image-generation"], + }, + { + id: "midjourney-turbo", + name: "Midjourney Turbo", + free: false, + auth: true, + tag: ["official", "fast", "image-generation"], + }, // reverse models - { id: "gpt-4-v", name: "GPT-4 Vision", free: false, auth: true }, - { id: "gpt-4-dalle", name: "GPT-4 DALLE", free: false, auth: true }, + { + id: "gpt-4-v", + name: "GPT-4 Vision", + free: false, + auth: true, + tag: ["official", "unstable", "multi-modal"], + }, + { + id: "gpt-4-dalle", + name: "GPT-4 DALLE", + free: false, + auth: true, + tag: ["official", "unstable", "image-generation"], + }, - // high price models - { id: "gpt-4-32k-0613", name: "GPT-4-32k", free: false, auth: true }, + { + id: "gpt-4-32k-0613", + name: "GPT-4-32k", + free: false, + auth: true, + 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", + + "claude-1-100k", + "claude-2", + + "spark-desk-v3", + "qwen-plus", + "hunyuan", + "zhipu-chatglm-turbo", + + "dall-e-2", + "midjourney-fast", + "stable-diffusion", ]; export const largeContextModels = [ @@ -101,7 +310,7 @@ export const largeContextModels = [ "zhipu-chatglm-turbo", ]; -export const studentModels = ["claude-1-100k", "claude-2-100k", "claude-2"]; +export const studentModels = ["claude-2-100k", "claude-2"]; export const planModels = [ "gpt-4-0613", @@ -110,7 +319,6 @@ export const planModels = [ "gpt-4-all", "gpt-4-dalle", "claude-2", - "claude-1-100k", "claude-2-100k", ]; @@ -120,6 +328,45 @@ export const expensiveModels = [ "gpt-4-32k-0613", ]; +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-4-0613": "gpt4.png", + "gpt-4-1106-preview": "gpt432k.webp", + "gpt-4-all": "gpt4.png", + "gpt-4-32k-0613": "gpt432k.webp", + "gpt-4-v": "gpt4v.png", + "gpt-4-dalle": "gpt4dalle.png", + "claude-1-100k": "claude.png", + "claude-2": "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", + "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", +}; + +export const modelPricingLink = "https://docs.chatnio.net/ai-mo-xing-ji-ji-fei"; + export function login() { location.href = `https://deeptrain.net/login?app=${dev ? "dev" : "chatnio"}`; } diff --git a/app/src/conversation/types.ts b/app/src/conversation/types.ts index 6f5db25..af3dc6a 100644 --- a/app/src/conversation/types.ts +++ b/app/src/conversation/types.ts @@ -13,6 +13,7 @@ export type Model = { name: string; free: boolean; auth: boolean; + tag?: string[]; }; export type Id = number; diff --git a/app/src/dialogs/Subscription.tsx b/app/src/dialogs/Subscription.tsx index 6b83e2a..745791d 100644 --- a/app/src/dialogs/Subscription.tsx +++ b/app/src/dialogs/Subscription.tsx @@ -55,7 +55,7 @@ import { useEffectAsync } from "@/utils/hook.ts"; import { selectAuthenticated } from "@/store/auth.ts"; function calc_prize(month: number): number { - const base = 32 * month; + const base = 56 * month; if (month >= 36) { return base * 0.7; } else if (month >= 12) { diff --git a/app/src/i18n.ts b/app/src/i18n.ts index 163d178..cd6ed0a 100644 --- a/app/src/i18n.ts +++ b/app/src/i18n.ts @@ -27,9 +27,28 @@ const resources = { "request-failed": "请求失败,请检查您的网络并重试。", close: "关闭", edit: "编辑", - pricing: "模型定价表", + pricing: "更多计费详情参见模型定价表", true: "是", false: "否", + tag: { + free: "免费", + official: "官方", + unstable: "不稳定", + web: "联网", + "high-quality": "高质量", + "high-context": "高上下文", + "high-price": "高定价", + "open-source": "开源", + "image-generation": "绘图", + "multi-modal": "多模态", + fast: "快速", + "english-model": "英文模型", + }, + market: { + model: "探索更多模型", + explore: "探索", + search: "搜索模型名称或者简介", + }, conversation: { title: "对话", empty: "空空如也", @@ -105,14 +124,14 @@ const resources = { free: "免费版", "free-price": "永久免费", pro: "专业版", - "pro-price": "32 元/月", + "pro-price": "56 元/月", "free-gpt3": "GPT-3.5 (16k) 永久免费", "free-dalle": "DALL·E 2 绘图永久免费", "free-web": "联网搜索功能", "free-conversation": "对话存储记录", "free-api": "API 调用", "pro-gpt4": "GPT-4 每日请求 100 次", - "pro-gpt4-desc": "(包含 GPT 4 Turbo, GPT 4V, DALL·E 3)", + "pro-gpt4-desc": "(包含 GPT 4 Turbo, GPT 4V, GPT 4 DALLE)", "pro-claude": "Claude 100k 每日请求 100 次", "pro-service": "优先服务支持", "pro-thread": "并发数提升", @@ -315,7 +334,7 @@ const resources = { "Request failed. Please check your network and try again.", close: "Close", edit: "Edit", - pricing: "Model Pricing", + pricing: "See model pricing for more details", true: "Yes", false: "No", conversation: { @@ -398,14 +417,14 @@ const resources = { free: "Free", "free-price": "Free Forever", pro: "Pro", - "pro-price": "32 CNY/Month", + "pro-price": "56 CNY/Month", "free-gpt3": "GPT-3.5 (16k) Free Forever", "free-dalle": "DALL·E 2 Free Forever", "free-web": "web searching feature", "free-conversation": "conversation storage", "free-api": "API calls", "pro-gpt4": "GPT-4 100 requests per day", - "pro-gpt4-desc": "(including GPT 4 Turbo, GPT 4V, DALL·E 3)", + "pro-gpt4-desc": "(including GPT 4 Turbo, GPT 4V, GPT 4 DALLE)", "pro-claude": "Claude 100k 100 requests per day", "pro-service": "Priority Service Support", "pro-thread": "Concurrency Increase", @@ -618,7 +637,8 @@ const resources = { "Ошибка запроса. Пожалуйста, проверьте свою сеть и попробуйте еще раз.", close: "Закрыть", edit: "Редактировать", - pricing: "Тарифы моделей", + pricing: + "См. ценообразование моделей для получения дополнительной информации", true: "Да", false: "Нет", conversation: { @@ -702,14 +722,14 @@ const resources = { free: "Бесплатно", "free-price": "Бесплатно навсегда", pro: "Профессиональный", - "pro-price": "32 CNY/месяц", + "pro-price": "56 CNY/месяц", "free-gpt3": "GPT-3.5 (16k) бесплатно навсегда", "free-dalle": "DALE·E 2 бесплатно навсегда", "free-web": "веб-поиск", "free-conversation": "хранение разговоров", "free-api": "API вызовы", "pro-gpt4": "GPT-4 100 запросов в день", - "pro-gpt4-desc": "(включая GPT 4 Turbo, GPT 4V, DALL·E 3)", + "pro-gpt4-desc": "(включая GPT 4 Turbo, GPT 4V, GPT 4 DALLE)", "pro-claude": "Claude 100k 100 запросов в день", "pro-service": "Приоритетная служба поддержки", "pro-thread": "Увеличение параллелизма", diff --git a/app/src/routes/Home.tsx b/app/src/routes/Home.tsx index c9b5b7a..5ec878c 100644 --- a/app/src/routes/Home.tsx +++ b/app/src/routes/Home.tsx @@ -2,12 +2,17 @@ import "@/assets/pages/home.less"; import "@/assets/pages/chat.less"; import ChatWrapper from "@/components/home/ChatWrapper.tsx"; import SideBar from "@/components/home/SideBar.tsx"; +import { useSelector } from "react-redux"; +import { selectMarket } from "@/store/chat.ts"; +import ModelMarket from "@/components/home/ModelMarket.tsx"; function Home() { + const market = useSelector(selectMarket); + return (
- + {market ? : }
); } diff --git a/app/src/routes/admin/Users.tsx b/app/src/routes/admin/Users.tsx index 2449e85..9ebddd5 100644 --- a/app/src/routes/admin/Users.tsx +++ b/app/src/routes/admin/Users.tsx @@ -7,13 +7,13 @@ import { import { useTranslation } from "react-i18next"; import InvitationTable from "@/components/admin/InvitationTable.tsx"; import UserTable from "@/components/admin/UserTable.tsx"; -import {mobile} from "@/utils/device.ts"; +import { mobile } from "@/utils/device.ts"; function Users() { const { t } = useTranslation(); return ( -
+
{t("admin.users")} diff --git a/app/src/store/chat.ts b/app/src/store/chat.ts index cabad7f..765e30f 100644 --- a/app/src/store/chat.ts +++ b/app/src/store/chat.ts @@ -3,7 +3,7 @@ import { ConversationInstance, Model } from "@/conversation/types.ts"; import { Message } from "@/conversation/types.ts"; import { insertStart } from "@/utils/base.ts"; import { RootState } from "./index.ts"; -import { supportModels } from "@/conf.ts"; +import { defaultModels, supportModels } from "@/conf.ts"; import { getMemory, setMemory } from "@/utils/memory.ts"; type initialStateType = { @@ -12,13 +12,25 @@ type initialStateType = { model: string; web: boolean; current: number; + model_list: string[]; + market: boolean; }; +function InModel(model: string): boolean { + return ( + model.length > 0 && + supportModels.filter((item: Model) => item.id === model).length > 0 + ); +} + function GetModel(model: string | undefined | null): string { - return model && - supportModels.filter((item: Model) => item.id === model).length - ? model - : supportModels[0].id; + return model && InModel(model) ? model : supportModels[0].id; +} + +function GetModelList(models: string | undefined | null): string[] { + return models && models.length + ? models.split(",").filter((item) => InModel(item)) + : defaultModels; } const chatSlice = createSlice({ @@ -29,6 +41,8 @@ const chatSlice = createSlice({ model: GetModel(getMemory("model")), web: getMemory("web") === "true", current: -1, + model_list: GetModelList(getMemory("model_list")), + market: false, } as initialStateType, reducers: { setHistory: (state, action) => { @@ -69,6 +83,33 @@ const chatSlice = createSlice({ setMessage: (state, action) => { state.messages[state.messages.length - 1] = action.payload as Message; }, + setModelList: (state, action) => { + setMemory("model_list", action.payload as string); + state.model_list = action.payload as string[]; + }, + addModelList: (state, action) => { + const model = action.payload as string; + if (InModel(model) && !state.model_list.includes(model)) { + state.model_list.push(model); + setMemory("model_list", state.model_list.join(",")); + } + }, + removeModelList: (state, action) => { + const model = action.payload as string; + if (InModel(model) && state.model_list.includes(model)) { + state.model_list = state.model_list.filter((item) => item !== model); + setMemory("model_list", state.model_list.join(",")); + } + }, + setMarket: (state, action) => { + state.market = action.payload as boolean; + }, + openMarket: (state) => { + state.market = true; + }, + closeMarket: (state) => { + state.market = false; + }, }, }); @@ -82,6 +123,12 @@ export const { setWeb, addMessage, setMessage, + setModelList, + addModelList, + removeModelList, + setMarket, + openMarket, + closeMarket, } = chatSlice.actions; export const selectHistory = (state: RootState): ConversationInstance[] => state.chat.history; @@ -90,5 +137,8 @@ export const selectMessages = (state: RootState): Message[] => export const selectModel = (state: RootState): string => state.chat.model; export const selectWeb = (state: RootState): boolean => state.chat.web; export const selectCurrent = (state: RootState): number => state.chat.current; +export const selectModelList = (state: RootState): string[] => + state.chat.model_list; +export const selectMarket = (state: RootState): boolean => state.chat.market; export default chatSlice.reducer; diff --git a/app/src/utils/base.ts b/app/src/utils/base.ts index ca14ef9..862e5dd 100644 --- a/app/src/utils/base.ts +++ b/app/src/utils/base.ts @@ -34,3 +34,11 @@ export function getNumber(value: string, supportNegative = true): string { export function parseNumber(value: string): number { return parseFloat(getNumber(value)); } + +export function splitList(value: string, separators: string[]): string[] { + const result: string[] = []; + for (const item of value.split(new RegExp(separators.join("|"), "g"))) { + if (item) result.push(item); + } + return result; +} diff --git a/auth/subscription.go b/auth/subscription.go index d12afde..3541ed5 100644 --- a/auth/subscription.go +++ b/auth/subscription.go @@ -8,7 +8,7 @@ import ( ) func CountSubscriptionPrize(month int) float32 { - base := 32 * float32(month) + base := 56 * float32(month) if month >= 36 { return base * 0.7 } else if month >= 12 {