add model market feature

This commit is contained in:
Zhang Minghan 2023-11-15 12:47:17 +08:00
parent c31a1c954d
commit 76b91c541f
42 changed files with 1009 additions and 115 deletions

View File

@ -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),

View File

@ -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"),

View File

@ -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",

89
app/pnpm-lock.yaml generated
View File

@ -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:

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
app/public/icons/claude.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
app/public/icons/dalle.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
app/public/icons/gpt4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
app/public/icons/gpt4v.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
app/public/icons/palm2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
app/public/icons/tongyi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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 (
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
remarkPlugins={[remarkMath, remarkGfm, remarkBreaks]}
rehypePlugins={[rehypeKatex]}
className={`markdown-body ${className}`}
children={children}

View File

@ -6,7 +6,7 @@ import {
SelectValue,
} from "./ui/select";
import { mobile } from "@/utils/device.ts";
import { useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { Badge } from "./ui/badge.tsx";
export type SelectItemBadgeProps = {
@ -19,6 +19,7 @@ export type SelectItemProps = {
value: string;
badge?: SelectItemBadgeProps;
tag?: any;
icon?: React.ReactNode;
};
type SelectGroupProps = {
@ -34,7 +35,8 @@ type SelectGroupProps = {
function GroupSelectItem(props: SelectItemProps) {
return (
<div className={`mr-1`}>
<div className={`mr-1 flex flex-row items-center align-center`}>
{props.icon && <div className={`mr-1`}>{props.icon}</div>}
{props.value}
{props.badge && (
<Badge

View File

@ -82,15 +82,6 @@ function ChatSpace() {
</DialogContent>
</Dialog>
<div className={`space-footer`}>
<p>
<Link className={`h-3 w-3 mr-1`} />
<a
href={`https://docs.chatnio.net/ai-mo-xing-ji-ji-fei`}
target={`_blank`}
>
{t("pricing")}
</a>
</p>
{cn && (
<p>

View File

@ -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 ? (
<ChatInterface setTarget={setInstance} />
) : (
<ChatSpace />
);
}
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 (
<div className={`chat-container`}>
<div className={`chat-wrapper`}>
{messages.length > 0 ? (
<ChatInterface setTarget={setInstance} />
) : (
<ChatSpace />
)}
<Interface setInstance={setInstance} />
<ScrollAction target={instance} />
<div className={`chat-input`}>
<div className={`input-wrapper`}>

View File

@ -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());
}}
>
<MessageSquare className={`h-4 w-4 mr-1`} />

View File

@ -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: <Sparkles size={16} />,
name: "market",
value: t("market.model"),
},
];
}, [supportModels, subscription, student]);
return (
<SelectGroup
current={list.find((item) => 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})`);

View File

@ -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 (
<div className={`search-bar`}>
<Search size={16} className={`search-icon`} />
<Input
placeholder={t("market.search")}
className={`rounded-full input-box`}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
<X
size={16}
className={`clear-icon ${value.length > 0 ? "active" : ""}`}
onClick={() => onChange("")}
/>
</div>
);
}
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 (
<div className={`model-item ${className}`} style={style} onClick={() => {
dispatch(addModelList(model.id));
dispatch(setModel(model.id));
dispatch(closeMarket());
}}>
<img className={`model-avatar`} src={avatar} alt={model.name} />
<div className={`model-info`}>
<p className={`model-name ${pro ? 'pro' : ''}`}>{model.name}</p>
<div className={`model-tag`}>
{model.tag &&
model.tag.map((tag, index) => {
return (
<span className={`tag-item`} key={index}>
{t(`tag.${tag}`)}
</span>
);
})}
</div>
</div>
<div className={`grow`} />
<div className={`model-action`}>
<Button size={`icon`} variant={`ghost`} className={`scale-90`} onClick={(e) => {
e.stopPropagation();
e.preventDefault();
if (state === 0) dispatch(closeMarket());
else if (state === 1) dispatch(removeModelList(model.id));
else dispatch(addModelList(model.id));
}}>
{
state === 0 ? (
<ChevronRight className={`h-4 w-4`} />
) : (
state === 1 ? (
<Trash2 className={`w-4 h-4`} />
) : (
<Plus className={`w-4 h-4`} />
)
)
}
</Button>
</div>
</div>
);
}
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 (
<div className={`model-list`}>
{arr.map((model, index) => (
<ModelItem
model={model}
key={index}
className={`${select === model.id ? "active" : ""}`}
/>
))}
</div>
);
}
function MarketHeader() {
const { t } = useTranslation();
const dispatch = useDispatch();
return (
<div className={`market-header`}>
<Button size={`icon`} variant={`ghost`} className={`close-action`} onClick={() => {
dispatch(closeMarket());
}}>
<ChevronLeft className={`h-4 w-4`} />
</Button>
<p className={`title select-none text-center text-primary font-bold`}>
{t("market.explore")}
</p>
</div>
)
}
function MarketFooter() {
const { t } = useTranslation();
return (
<div className={`market-footer`}>
<a href={modelPricingLink} target={`_blank`}>
<Link size={14} className={`mr-1`} />
{t("pricing")}
</a>
</div>
);
}
function ModelMarket() {
const [search, setSearch] = useState<string>("");
return (
<div className={`model-market`}>
<MarketHeader />
<SearchBar value={search} onChange={setSearch} />
<MarketPlace search={search} />
<MarketFooter />
</div>
);
}
export default ModelMarket;

View File

@ -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());
}}
>
<Plus className={`h-4 w-4`} />

View File

@ -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<string, string> = {
"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"}`;
}

View File

@ -13,6 +13,7 @@ export type Model = {
name: string;
free: boolean;
auth: boolean;
tag?: string[];
};
export type Id = number;

View File

@ -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) {

View File

@ -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": "Увеличение параллелизма",

View File

@ -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 (
<div className={`main`}>
<SideBar />
<ChatWrapper />
{market ? <ModelMarket /> : <ChatWrapper />}
</div>
);
}

View File

@ -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 (
<div className={`user-interface ${mobile ? 'mobile' : ''}`}>
<div className={`user-interface ${mobile ? "mobile" : ""}`}>
<Card>
<CardHeader className={`select-none`}>
<CardTitle>{t("admin.users")}</CardTitle>

View File

@ -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;

View File

@ -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;
}

View File

@ -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 {