feat release: admin model market feature

This commit is contained in:
Zhang Minghan 2024-01-09 23:52:52 +08:00
parent b3d406858b
commit 6f6d818197
22 changed files with 293 additions and 579 deletions

View File

@ -33,6 +33,30 @@ type UpdateRootPasswordForm struct {
Password string `json:"password"` Password string `json:"password"`
} }
func UpdateMarketAPI(c *gin.Context) {
var form MarketModelList
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
err := MarketInstance.SetModels(form)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"status": false,
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": true,
})
}
func InfoAPI(c *gin.Context) { func InfoAPI(c *gin.Context) {
db := utils.GetDBFromContext(c) db := utils.GetDBFromContext(c)
cache := utils.GetCacheFromContext(c) cache := utils.GetCacheFromContext(c)

7
admin/instance.go Normal file
View File

@ -0,0 +1,7 @@
package admin
var MarketInstance *Market
func InitInstance() {
MarketInstance = NewMarket()
}

57
admin/market.go Normal file
View File

@ -0,0 +1,57 @@
package admin
import (
"fmt"
"github.com/spf13/viper"
)
type ModelTag []string
type MarketModel struct {
Id string `json:"id" mapstructure:"id" required:"true"`
Name string `json:"name" mapstructure:"name" required:"true"`
Description string `json:"description" mapstructure:"description"`
Default bool `json:"default" mapstructure:"default"`
HighContext bool `json:"high_context" mapstructure:"high_context"`
Avatar string `json:"avatar" mapstructure:"avatar"`
Tag ModelTag `json:"tag" mapstructure:"tag"`
}
type MarketModelList []MarketModel
type Market struct {
Models MarketModelList `json:"models" mapstructure:"models"`
}
func NewMarket() *Market {
var models MarketModelList
if err := viper.UnmarshalKey("market", &models); err != nil {
fmt.Println(fmt.Sprintf("[market] read config error: %s, use default config", err.Error()))
models = MarketModelList{}
}
return &Market{
Models: models,
}
}
func (m *Market) GetModels() MarketModelList {
return m.Models
}
func (m *Market) GetModel(id string) *MarketModel {
for _, model := range m.Models {
if model.Id == id {
return &model
}
}
return nil
}
func (m *Market) SaveConfig() error {
viper.Set("market", m.Models)
return viper.WriteConfig()
}
func (m *Market) SetModels(models MarketModelList) error {
m.Models = models
return m.SaveConfig()
}

View File

@ -24,4 +24,6 @@ func Register(app *gin.RouterGroup) {
app.POST("/admin/user/quota", UserQuotaAPI) app.POST("/admin/user/quota", UserQuotaAPI)
app.POST("/admin/user/subscription", UserSubscriptionAPI) app.POST("/admin/user/subscription", UserSubscriptionAPI)
app.POST("/admin/user/root", UpdateRootPasswordAPI) app.POST("/admin/user/root", UpdateRootPasswordAPI)
app.POST("/admin/market/update", UpdateMarketAPI)
} }

33
app/src/api/v1.ts Normal file
View File

@ -0,0 +1,33 @@
import axios from "axios";
import { Model } from "@/api/types.ts";
import { ChargeProps } from "@/admin/charge.ts";
export async function getApiModels(): Promise<string[]> {
try {
const res = await axios.get("/v1/models");
return res.data as string[];
} catch (e) {
console.warn(e);
return [];
}
}
export async function getApiMarket(): Promise<Model[]> {
try {
const res = await axios.get("/v1/market");
return res.data as Model[];
} catch (e) {
console.warn(e);
return [];
}
}
export async function getApiCharge(): Promise<ChargeProps[]> {
try {
const res = await axios.get("/v1/charge");
return res.data as ChargeProps[];
} catch (e) {
console.warn(e);
return [];
}
}

View File

@ -21,7 +21,7 @@
width: 100%; width: 100%;
height: max-content; height: max-content;
padding: 1rem 2rem; padding: 1rem 2rem 0;
@media (max-width: 940px) { @media (max-width: 940px) {
flex-direction: column; flex-direction: column;
@ -102,9 +102,18 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
padding: 1rem 1.5rem; padding: 0 1.5rem 1rem;
width: 100%; width: 100%;
@media (max-width: 940px) {
flex-direction: column;
padding: 1rem;
.chart-box {
width: calc(100% - 1rem) !important;
}
}
.chart-box { .chart-box {
width: calc(50% - 1rem); width: calc(50% - 1rem);
height: max-content; height: max-content;
@ -116,13 +125,5 @@
background: hsl(var(--background)); background: hsl(var(--background));
box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow); box-shadow: 0.5rem 0.5rem 1rem 0 var(--shadow);
user-select: none; user-select: none;
@media (max-width: 680px) {
width: 100%;
}
}
@media (max-width: 680px) {
padding: 1rem;
} }
} }

View File

@ -43,6 +43,11 @@
height: max-content; height: max-content;
align-items: center; align-items: center;
background: hsl(var(--card)); background: hsl(var(--card));
transition: 0.25s;
&.error {
border-color: hsl(var(--error));
}
.market-tags { .market-tags {
display: flex; display: flex;

View File

@ -28,7 +28,7 @@
height: max-content; height: max-content;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
align-items: center; align-items: center;
margin: 0 0.75rem; margin: 0.15rem 0.75rem;
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
transition: 0.2s ease-in-out; transition: 0.2s ease-in-out;
@ -36,35 +36,13 @@
font-size: 16px; font-size: 16px;
color: hsl(var(--text-secondary)); color: hsl(var(--text-secondary));
&:before {
content: '';
display: inline-block;
width: 4px;
height: 2rem;
background: hsl(var(--text));
border-radius: var(--radius);
margin-right: 0;
opacity: 0;
transition: 0.25s;
transition-property: opacity, margin-right;
}
&:hover { &:hover {
background: hsl(var(--card-hover)); background: hsl(var(--card-hover));
&:before {
opacity: .05;
margin-right: 0.75rem;
}
} }
&.active { &.active {
color: hsl(var(--text)); color: hsl(var(--text));
background: hsl(var(--card-hover));
&:before {
opacity: 1;
margin-right: 0.75rem;
}
} }
& > * { & > * {
@ -78,7 +56,10 @@
.menu-item-icon { .menu-item-icon {
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
scale: 0.95;
margin-right: 0.5rem; margin-right: 0.5rem;
margin-left: 0.5rem;
transform: translateY(1px);
} }
} }

View File

@ -60,6 +60,7 @@
--gold: 45 100% 50%; --gold: 45 100% 50%;
--link: 210 100% 63%; --link: 210 100% 63%;
--error: 20 80% 50%;
} }
.dark { .dark {

View File

@ -3,9 +3,9 @@ import { closeMenu, selectMenu } from "@/store/menu.ts";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { import {
BookCopy, BookCopy,
CandlestickChart, CloudCog,
Gauge,
GitFork, GitFork,
LayoutDashboard,
Radio, Radio,
Settings, Settings,
Users, Users,
@ -49,11 +49,7 @@ function MenuBar() {
const open = useSelector(selectMenu); const open = useSelector(selectMenu);
return ( return (
<div className={`admin-menu ${open ? "open" : ""}`}> <div className={`admin-menu ${open ? "open" : ""}`}>
<MenuItem <MenuItem title={t("admin.dashboard")} icon={<Gauge />} path={"/"} />
title={t("admin.dashboard")}
icon={<LayoutDashboard />}
path={"/"}
/>
<MenuItem title={t("admin.user")} icon={<Users />} path={"/users"} /> <MenuItem title={t("admin.user")} icon={<Users />} path={"/users"} />
<MenuItem <MenuItem
title={t("admin.market.title")} title={t("admin.market.title")}
@ -70,11 +66,7 @@ function MenuBar() {
icon={<GitFork />} icon={<GitFork />}
path={"/channel"} path={"/channel"}
/> />
<MenuItem <MenuItem title={t("admin.prize")} icon={<CloudCog />} path={"/charge"} />
title={t("admin.prize")}
icon={<CandlestickChart />}
path={"/charge"}
/>
<MenuItem <MenuItem
title={t("admin.settings")} title={t("admin.settings")}
icon={<Settings />} icon={<Settings />}

View File

@ -3,14 +3,40 @@ import { ThemeProvider } from "@/components/ThemeProvider.tsx";
import DialogManager from "@/dialogs"; import DialogManager from "@/dialogs";
import Broadcast from "@/components/Broadcast.tsx"; import Broadcast from "@/components/Broadcast.tsx";
import { useEffectAsync } from "@/utils/hook.ts"; import { useEffectAsync } from "@/utils/hook.ts";
import { allModels } from "@/conf.ts"; import { allModels, supportModels } from "@/conf.ts";
import axios from "axios";
import { channelModels } from "@/admin/channel.ts"; import { channelModels } from "@/admin/channel.ts";
import { getApiCharge, getApiMarket, getApiModels } from "@/api/v1.ts";
import { loadPreferenceModels } from "@/utils/storage.ts";
import { resetJsArray } from "@/utils/base.ts";
import { useDispatch } from "react-redux";
import { initChatModels } from "@/store/chat.ts";
import { Model } from "@/api/types.ts";
import { ChargeProps, nonBilling } from "@/admin/charge.ts";
function AppProvider() { function AppProvider() {
const dispatch = useDispatch();
useEffectAsync(async () => { useEffectAsync(async () => {
const res = await axios.get("/v1/models"); const market = await getApiMarket();
res.data.forEach((model: string) => { const charge = await getApiCharge();
market.forEach((item: Model) => {
const obj = charge.find((i: ChargeProps) => i.models.includes(item.id));
if (!obj) return;
item.free = obj.type === nonBilling;
item.auth = item.free && !obj.anonymous;
});
resetJsArray(supportModels, loadPreferenceModels(market));
resetJsArray(
allModels,
supportModels.map((model) => model.id),
);
initChatModels(dispatch);
const models = await getApiModels();
models.forEach((model: string) => {
if (!allModels.includes(model)) allModels.push(model); if (!allModels.includes(model)) allModels.push(model);
if (!channelModels.includes(model)) channelModels.push(model); if (!channelModels.includes(model)) channelModels.push(model);
}); });

View File

@ -18,6 +18,7 @@ const TooltipContent = React.forwardRef<
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"max-w-[100vw]",
className, className,
)} )}
{...props} {...props}

View File

@ -12,7 +12,7 @@ import { getMemory } from "@/utils/memory.ts";
import { Compass, Image, Newspaper } from "lucide-react"; import { Compass, Image, Newspaper } from "lucide-react";
import React from "react"; import React from "react";
import { syncSiteInfo } from "@/admin/api/info.ts"; import { syncSiteInfo } from "@/admin/api/info.ts";
import { loadPreferenceModels } from "@/utils/storage.ts"; import { getOfflineModels, loadPreferenceModels } from "@/utils/storage.ts";
export const version = "3.8.1"; export const version = "3.8.1";
export const dev: boolean = getDev(); export const dev: boolean = getDev();
@ -21,517 +21,7 @@ export let rest_api: string = getRestApi(deploy);
export let ws_api: string = getWebsocketApi(deploy); export let ws_api: string = getWebsocketApi(deploy);
export const tokenField = getTokenField(deploy); export const tokenField = getTokenField(deploy);
export let supportModels: Model[] = loadPreferenceModels([ export let supportModels: Model[] = loadPreferenceModels(getOfflineModels());
// 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"],
},
// spark desk
{
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"],
},
// dashscope models
{
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"],
},
// huyuan models
{
id: "hunyuan",
name: "腾讯混元 Pro",
avatar: "hunyuan.png",
free: false,
auth: true,
high_context: false,
default: true,
tag: ["official"],
},
// zhipu models
{
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"],
},
// baichuan models
{
id: "baichuan-53b",
name: "百川 Baichuan 53B",
avatar: "baichuan.png",
free: false,
auth: true,
high_context: false,
default: true,
tag: ["official", "open-source"],
},
// skylark models
{
id: "skylark-chat",
name: "抖音豆包 Skylark",
avatar: "skylark.jpg",
free: false,
auth: true,
high_context: false,
default: true,
tag: ["official"],
},
// 360 models
{
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"],
},
// llama models
{
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"],
},
// new bing
{
id: "bing-creative",
name: "New Bing",
avatar: "newbing.jpg",
free: true,
auth: true,
high_context: true,
default: true,
tag: ["free", "unstable", "web"],
},
// google palm2
{
id: "chat-bison-001",
name: "Google PaLM2",
avatar: "palm2.webp",
free: true,
auth: true,
high_context: false,
default: false,
tag: ["free", "english-model"],
},
// gemini
{
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"],
},
// drawing models
{
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 let allModels: string[] = supportModels.map((model) => model.id); export let allModels: string[] = supportModels.map((model) => model.id);

View File

@ -17,7 +17,7 @@ import { Model as RawModel } from "@/api/types.ts";
import { supportModels } from "@/conf.ts"; import { supportModels } from "@/conf.ts";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd"; import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
import { Input } from "@/components/ui/input.tsx"; import { Input } from "@/components/ui/input.tsx";
import { GripVertical, HelpCircle } from "lucide-react"; import { GripVertical, HelpCircle, Plus, Trash2 } from "lucide-react";
import { generateRandomChar, isUrl } from "@/utils/base.ts"; import { generateRandomChar, isUrl } from "@/utils/base.ts";
import Require from "@/components/Require.tsx"; import Require from "@/components/Require.tsx";
import { Textarea } from "@/components/ui/textarea.tsx"; import { Textarea } from "@/components/ui/textarea.tsx";
@ -59,6 +59,22 @@ function reducer(state: MarketForm, action: any): MarketForm {
seed: generateSeed(), seed: generateSeed(),
}, },
]; ];
case "new":
return [
...state,
{
id: "",
name: "",
free: false,
auth: false,
description: "",
high_context: false,
default: false,
tag: [],
avatar: modelImages[0],
seed: generateSeed(),
},
];
case "remove": case "remove":
let { idx } = action.payload; let { idx } = action.payload;
return [...state.slice(0, idx), ...state.slice(idx + 1)]; return [...state.slice(0, idx), ...state.slice(idx + 1)];
@ -342,6 +358,14 @@ function Market() {
); );
}, [form]); }, [form]);
const doCheck = (index: number) => {
return useMemo((): boolean => {
const model = form[index];
return model.id.trim().length > 0 && model.name.trim().length > 0;
}, [form, index]);
};
return ( return (
<div className={`market`}> <div className={`market`}>
<Card className={`market-card`}> <Card className={`market-card`}>
@ -349,7 +373,7 @@ function Market() {
<CardTitle>{t("admin.market.title")}</CardTitle> <CardTitle>{t("admin.market.title")}</CardTitle>
<Button <Button
loading={true} loading={true}
className={`ml-auto mt-0`} className={`ml-auto mt-0 whitespace-nowrap`}
size={`sm`} size={`sm`}
style={{ marginTop: 0 }} style={{ marginTop: 0 }}
onClick={update} onClick={update}
@ -389,7 +413,9 @@ function Market() {
> >
{(provided) => ( {(provided) => (
<div <div
className={`market-item`} className={`market-item ${
doCheck(index) ? "" : "error"
}`}
ref={provided.innerRef} ref={provided.innerRef}
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
@ -515,6 +541,29 @@ function Market() {
dispatch={dispatch} dispatch={dispatch}
/> />
</div> </div>
<div className={`market-row`}>
<div className={`grow`} />
<Button
variant={`outline`}
size={`icon`}
onClick={() =>
dispatch({
type: "remove",
payload: { idx: index },
})
}
>
<Trash2 className={`h-4 w-4`} />
</Button>
{index === form.length - 1 && (
<Button
size={`icon`}
onClick={() => dispatch({ type: "new" })}
>
<Plus className={`h-4 w-4`} />
</Button>
)}
</div>
</div> </div>
</div> </div>
)} )}

View File

@ -2,9 +2,16 @@ import { createSlice } from "@reduxjs/toolkit";
import { ConversationInstance, Model } from "@/api/types.ts"; import { ConversationInstance, Model } from "@/api/types.ts";
import { Message } from "@/api/types.ts"; import { Message } from "@/api/types.ts";
import { insertStart } from "@/utils/base.ts"; import { insertStart } from "@/utils/base.ts";
import { RootState } from "./index.ts"; import { AppDispatch, RootState } from "./index.ts";
import { planModels, supportModels } from "@/conf.ts"; import { planModels, supportModels } from "@/conf.ts";
import { getBooleanMemory, getMemory, setMemory } from "@/utils/memory.ts"; import {
getArrayMemory,
getBooleanMemory,
getMemory,
setArrayMemory,
setMemory,
} from "@/utils/memory.ts";
import { setOfflineModels } from "@/utils/storage.ts";
type initialStateType = { type initialStateType = {
history: ConversationInstance[]; history: ConversationInstance[];
@ -31,17 +38,12 @@ export function getPlanModels(level: number): string[] {
} }
export function getModel(model: string | undefined | null): string { export function getModel(model: string | undefined | null): string {
if (supportModels.length === 0) return "";
return model && inModel(model) ? model : supportModels[0].id; return model && inModel(model) ? model : supportModels[0].id;
} }
export function getModelList( export function getModelList(models: string[], select: string): string[] {
models: string | undefined | null, const list = models.filter((item) => inModel(item));
select: string | undefined | null,
): string[] {
const list =
models && models.length
? models.split(",").filter((item) => inModel(item))
: [];
const target = list.length const target = list.length
? list ? list
: supportModels.filter((item) => item.default).map((item) => item.id); : supportModels.filter((item) => item.default).map((item) => item.id);
@ -58,11 +60,20 @@ const chatSlice = createSlice({
model: getModel(getMemory("model")), model: getModel(getMemory("model")),
web: getBooleanMemory("web", false), web: getBooleanMemory("web", false),
current: -1, current: -1,
model_list: getModelList(getMemory("model_list"), getMemory("model")), model_list: getModelList(getArrayMemory("model_list"), getMemory("model")),
market: false, market: false,
mask: false, mask: false,
} as initialStateType, } as initialStateType,
reducers: { reducers: {
doInit: (state) => {
setOfflineModels(supportModels);
state.model = getModel(getMemory("model"));
state.model_list = getModelList(
getArrayMemory("model_list"),
getMemory("model"),
);
},
setHistory: (state, action) => { setHistory: (state, action) => {
state.history = action.payload as ConversationInstance[]; state.history = action.payload as ConversationInstance[];
}, },
@ -109,20 +120,20 @@ const chatSlice = createSlice({
setModelList: (state, action) => { setModelList: (state, action) => {
const models = action.payload as string[]; const models = action.payload as string[];
state.model_list = models.filter((item) => inModel(item)); state.model_list = models.filter((item) => inModel(item));
setMemory("model_list", models.join(",")); setArrayMemory("model_list", models);
}, },
addModelList: (state, action) => { addModelList: (state, action) => {
const model = action.payload as string; const model = action.payload as string;
if (inModel(model) && !state.model_list.includes(model)) { if (inModel(model) && !state.model_list.includes(model)) {
state.model_list.push(model); state.model_list.push(model);
setMemory("model_list", state.model_list.join(",")); setArrayMemory("model_list", state.model_list);
} }
}, },
removeModelList: (state, action) => { removeModelList: (state, action) => {
const model = action.payload as string; const model = action.payload as string;
if (inModel(model) && state.model_list.includes(model)) { if (inModel(model) && state.model_list.includes(model)) {
state.model_list = state.model_list.filter((item) => item !== model); state.model_list = state.model_list.filter((item) => item !== model);
setMemory("model_list", state.model_list.join(",")); setArrayMemory("model_list", state.model_list);
} }
}, },
setMarket: (state, action) => { setMarket: (state, action) => {
@ -147,6 +158,7 @@ const chatSlice = createSlice({
}); });
export const { export const {
doInit,
setHistory, setHistory,
removeHistory, removeHistory,
addHistory, addHistory,
@ -178,5 +190,6 @@ export const selectModelList = (state: RootState): string[] =>
state.chat.model_list; state.chat.model_list;
export const selectMarket = (state: RootState): boolean => state.chat.market; export const selectMarket = (state: RootState): boolean => state.chat.market;
export const selectMask = (state: RootState): boolean => state.chat.mask; export const selectMask = (state: RootState): boolean => state.chat.mask;
export const initChatModels = (dispatch: AppDispatch) => dispatch(doInit());
export default chatSlice.reducer; export default chatSlice.reducer;

View File

@ -71,3 +71,12 @@ export function isUrl(value: string): boolean {
return false; return false;
} }
} }
export function resetJsArray<T>(arr: T[], target: T[]): T[] {
/**
* this function is used to reset an array to another array without changing the *pointer
*/
arr.splice(0, arr.length, ...target);
return arr;
}

View File

@ -11,6 +11,10 @@ export function setNumberMemory(key: string, value: number) {
setMemory(key, value.toString()); setMemory(key, value.toString());
} }
export function setArrayMemory(key: string, value: string[]) {
setMemory(key, value.join(","));
}
export function getMemory(key: string): string { export function getMemory(key: string): string {
return (localStorage.getItem(key) || "").trim(); return (localStorage.getItem(key) || "").trim();
} }

View File

@ -25,3 +25,12 @@ export function loadPreferenceModels(models: Model[]): Model[] {
return aIndex - bIndex; return aIndex - bIndex;
}); });
} }
export function setOfflineModels(models: Model[]): void {
setMemory("model_offline", JSON.stringify(models));
}
export function getOfflineModels(): Model[] {
const memory = getMemory("model_offline");
return memory.length ? (JSON.parse(memory) as Model[]) : [];
}

View File

@ -45,6 +45,9 @@ func ConnectMySQL() *sql.DB {
log.Println(fmt.Sprintf("[connection] connected to mysql server (host: %s)", viper.GetString("mysql.host"))) log.Println(fmt.Sprintf("[connection] connected to mysql server (host: %s)", viper.GetString("mysql.host")))
} }
db.SetMaxOpenConns(512)
db.SetMaxIdleConns(64)
CreateUserTable(db) CreateUserTable(db)
CreateConversationTable(db) CreateConversationTable(db)
CreateSharingTable(db) CreateSharingTable(db)

View File

@ -36,6 +36,7 @@ func registerApiRouter(engine *gin.Engine) {
func main() { func main() {
utils.ReadConf() utils.ReadConf()
admin.InitInstance()
channel.InitManager() channel.InitManager()
if cli.Run() { if cli.Run() {

View File

@ -1,6 +1,7 @@
package manager package manager
import ( import (
"chat/admin"
"chat/channel" "chat/channel"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
@ -10,6 +11,10 @@ func ModelAPI(c *gin.Context) {
c.JSON(http.StatusOK, channel.ConduitInstance.GetModels()) c.JSON(http.StatusOK, channel.ConduitInstance.GetModels())
} }
func MarketAPI(c *gin.Context) {
c.JSON(http.StatusOK, admin.MarketInstance.GetModels())
}
func ChargeAPI(c *gin.Context) { func ChargeAPI(c *gin.Context) {
c.JSON(http.StatusOK, channel.ChargeInstance.ListRules()) c.JSON(http.StatusOK, channel.ChargeInstance.ListRules())
} }

View File

@ -8,6 +8,7 @@ import (
func Register(app *gin.RouterGroup) { func Register(app *gin.RouterGroup) {
app.GET("/chat", ChatAPI) app.GET("/chat", ChatAPI)
app.GET("/v1/models", ModelAPI) app.GET("/v1/models", ModelAPI)
app.GET("/v1/market", MarketAPI)
app.GET("/v1/charge", ChargeAPI) app.GET("/v1/charge", ChargeAPI)
app.GET("/dashboard/billing/usage", GetBillingUsage) app.GET("/dashboard/billing/usage", GetBillingUsage)
app.GET("/dashboard/billing/subscription", GetSubscription) app.GET("/dashboard/billing/subscription", GetSubscription)