Skip to content

Commit

Permalink
feat: support drag-and-drop placement in the model marketplace
Browse files Browse the repository at this point in the history
  • Loading branch information
zmh-program committed Mar 11, 2024
1 parent 825009f commit dcc2572
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 58 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ _🚀 **Next Generation AI One-Stop Solution**_
- 举个例子, 前端使用 Nginx (或 Vercel 等) 部署, 部署的域名为 `https://www.chatnio.net`
- 后端使用 Docker 部署, 部署的域名为 `https://api.chatnio.net`
- 此种部署方式需自行打包前端, 配置环境变量 `VITE_BACKEND_ENDPOINT` 为你的后端地址, 如 `https://api.chatnio.net`
- 配置后端环境变量的 `SERVE_STATIC``false`, 并配置 `ALLOW_ORIGINS` 为你的前端地址, 如 `chatnio.net` (不需要加协议前缀, www 解析无需手动添加, 后端将自动识别并允许跨域)
- 配置后端环境变量的 `SERVE_STATIC=false` 使后端服务不提供静态文件服务
11. **弹性计费和订阅详解**
- 弹性计费, 即 `点数`, 其图标类似于****, 模型计费通用方式, 为了防止虚假汇率, 写死 10 点数 = 1 元, 汇率可以在计费规则中的 **应用内置模板** 中自定义汇率。
- 订阅, 即订阅计划, 为固定价格计费方式按次配额, 订阅计费扣取点数 (举例: 如果站点的用户想订阅 32 元的计划, 则需要保证点数大于等于 320 点数)
Expand All @@ -249,7 +249,10 @@ _🚀 **Next Generation AI One-Stop Solution**_
- 此联网搜索通过预设实现, 意为保证全模型都支持的通用功能, 兼容性无法保证灵敏性, 不依赖模型 Function Calling, 其他本身支持联网的模型可以选择直接关闭此功能。
14. **为何我的 GPT-4-All 等逆向模型无法使用上传文件中的图片?**
- 上传模型图片为 Base64 格式, 如果逆向不支持 Base64 格式, 请使用 URL 直链而非上传文件做法。

15. **如何开始域名严格跨域检测?**
- 正常情况下,后端对所有域名开放跨域。如果非特殊需求,无需开启严格跨域检测。
- 如果需要开启严格跨域检测,可以在后端环境变量中 并配置 `ALLOW_ORIGINS`, 如 `ALLOW_ORIGINS=chatnio.net,chatnio.app` (不需要加协议前缀, www 解析无需手动添加, 后端将自动识别并允许跨域), 这样就会支持严格跨域检测 (如 *http://www.chatnio.app*, *https://chatnio.net* 等将会被允许, 其他域名将会被拒绝)。
- 即使在开启严格跨域检测的情况下, /v1 接口会被仍然允许所有域的跨域请求, 以保证中转 API 的正常使用。
## 📦 技术栈
- 前端: React + Radix UI + Tailwind CSS + Shadcn + Tremor + Redux
- 后端: Golang + Gin + Redis + MySQL
Expand Down
1 change: 1 addition & 0 deletions app/src/assets/admin/market.less
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
align-items: center;
background: hsl(var(--card));
transition: 0.25s;
transition-property: border, background;

&.error {
border-color: hsl(var(--error));
Expand Down
200 changes: 149 additions & 51 deletions app/src/routes/admin/Market.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
CardTitle,
} from "@/components/ui/card.tsx";
import { useTranslation } from "react-i18next";
import { Dispatch, useMemo, useReducer, useState } from "react";
import React, { Dispatch, useMemo, useReducer, useState } from "react";
import { Model as RawModel } from "@/api/types.tsx";
import { Input } from "@/components/ui/input.tsx";
import {
Activity,
AlertCircle,
ChevronDown,
ChevronUp,
GripVertical,
HelpCircle,
Import,
Maximize,
Expand Down Expand Up @@ -48,6 +49,12 @@ import { updateMarket } from "@/admin/api/market.ts";
import { toast } from "sonner";
import { useChannelModels, useSupportModels } from "@/admin/hook.tsx";
import Icon from "@/components/utils/Icon.tsx";
import {
DragDropContext,
Draggable,
Droppable,
DropResult,
} from "react-beautiful-dnd";

type Model = RawModel & {
seed?: string;
Expand Down Expand Up @@ -108,7 +115,6 @@ function reducer(state: MarketForm, action: any): MarketForm {
];
case "new-template":
return [
...state,
{
id: action.payload.id,
name: action.payload.name,
Expand All @@ -121,10 +127,10 @@ function reducer(state: MarketForm, action: any): MarketForm {
avatar: modelImages[0],
seed: generateSeed(),
},
...state,
];
case "batch-new-template":
return [
...state,
...action.payload.map((model: { id: string; name: string }) => ({
id: model.id,
name: model.name,
Expand All @@ -137,6 +143,7 @@ function reducer(state: MarketForm, action: any): MarketForm {
avatar: modelImages[0],
seed: generateSeed(),
})),
...state,
];
case "remove":
let { idx } = action.payload;
Expand Down Expand Up @@ -268,6 +275,12 @@ function reducer(state: MarketForm, action: any): MarketForm {
state[action.payload.idx] = state[action.payload.idx + 1];
state[action.payload.idx + 1] = downward;
return [...state];
case "move":
const { fromIndex, toIndex } = action.payload;
const moved = state[fromIndex];
state.splice(fromIndex, 1);
state.splice(toIndex, 0, moved);
return [...state];
default:
throw new Error();
}
Expand Down Expand Up @@ -400,13 +413,17 @@ function MarketImage({ image, idx, dispatch }: MarketImageProps) {
);
}

type MarketItemProps = {
type MarketItemProps = React.DetailedHTMLProps<
React.HTMLAttributes<HTMLDivElement>,
HTMLDivElement
> & {
model: Model;
form: MarketForm;
dispatch: Dispatch<any>;
index: number;
stacked: boolean;
channelModels: string[];
forwardRef?: React.Ref<HTMLDivElement>;
};

function MarketItem({
Expand All @@ -416,6 +433,8 @@ function MarketItem({
dispatch,
index,
channelModels,
forwardRef,
...props
}: MarketItemProps) {
const { t } = useTranslation();

Expand All @@ -424,7 +443,7 @@ function MarketItem({
[model],
);

const Actions = () => (
const Actions = ({ stacked }: { stacked?: boolean }) => (
<div className={`market-row`}>
{!stacked && <div className={`grow`} />}
<Button
Expand All @@ -440,32 +459,38 @@ function MarketItem({
<Plus className={`h-4 w-4`} />
</Button>

<Button
size={`icon`}
variant={`outline`}
onClick={() =>
dispatch({
type: "upward",
payload: { idx: index },
})
}
disabled={index === 0}
>
<ChevronUp className={`h-4 w-4`} />
</Button>
<Button
size={`icon`}
variant={`outline`}
onClick={() =>
dispatch({
type: "downward",
payload: { idx: index },
})
}
disabled={index === form.length - 1}
>
<ChevronDown className={`h-4 w-4`} />
</Button>
{!stacked && (
<Button
size={`icon`}
variant={`outline`}
onClick={() =>
dispatch({
type: "upward",
payload: { idx: index },
})
}
disabled={index === 0}
>
<ChevronUp className={`h-4 w-4`} />
</Button>
)}

{!stacked && (
<Button
size={`icon`}
variant={`outline`}
onClick={() =>
dispatch({
type: "downward",
payload: { idx: index },
})
}
disabled={index === form.length - 1}
>
<ChevronDown className={`h-4 w-4`} />
</Button>
)}

<Button
size={`icon`}
onClick={() =>
Expand All @@ -481,7 +506,11 @@ function MarketItem({
);

return !stacked ? (
<div className={cn("market-item", !checked && "error")}>
<div
className={cn("market-item", !checked && "error")}
{...props}
ref={forwardRef}
>
<div className={`model-wrapper`}>
<div className={`market-row`}>
<span>
Expand Down Expand Up @@ -586,7 +615,12 @@ function MarketItem({
</div>
</div>
) : (
<div className={cn("market-item stacked", !checked && "error")}>
<div
className={cn("market-item stacked", !checked && "error")}
{...props}
ref={forwardRef}
>
<GripVertical className={`h-4 w-4 mr-2 cursor-pointer`} />
<Input
value={model.name}
placeholder={t("admin.market.model-name-placeholder")}
Expand All @@ -601,11 +635,47 @@ function MarketItem({
});
}}
/>
<Actions />
<Actions stacked={true} />
</div>
);
}

type MarketGroupProps = {
form: MarketForm;
dispatch: Dispatch<any>;
stacked: boolean;
channelModels: string[];
};
function MarketGroup({
form,
dispatch,
stacked,
channelModels,
}: MarketGroupProps) {
return form.map((model, index) => (
<Draggable
key={model.seed as string}
draggableId={model.seed as string}
index={index}
>
{(provided) => (
<MarketItem
key={index}
model={model}
form={form}
stacked={stacked}
dispatch={dispatch}
index={index}
channelModels={channelModels}
forwardRef={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
/>
)}
</Draggable>
));
}

type SyncDialogProps = {
open: boolean;
setOpen: (state: boolean) => void;
Expand Down Expand Up @@ -844,6 +914,27 @@ function Market() {

const sync = async (): Promise<void> => {};

const onDragEnd = (result: DropResult) => {
const { destination, source } = result;
if (
!destination ||
destination.index === source.index ||
destination.index === -1
)
return;

const from = source.index;
const to = destination.index;

dispatch({
type: "move",
payload: {
fromIndex: from,
toIndex: to,
},
});
};

const submit = async (): Promise<void> => {
const preflight = form.filter(
(model) => model.id.trim().length > 0 && model.name.trim().length > 0,
Expand Down Expand Up @@ -953,23 +1044,30 @@ function Market() {
});
}}
/>
<div className={`market-list`}>
{form.length > 0 ? (
form.map((model, index) => (
<MarketItem
key={index}
model={model}
form={form}
stacked={stacked}
dispatch={dispatch}
index={index}
channelModels={channelModels}
/>
))
) : (
<p className={`align-center text-sm empty`}>{t("admin.empty")}</p>
)}
</div>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId={`market-list`}>
{(provided) => (
<div
className={`market-list cursor-default`}
{...provided.droppableProps}
ref={provided.innerRef}
>
{form.length > 0 ? (
<MarketGroup
form={form}
dispatch={dispatch}
stacked={stacked}
channelModels={channelModels}
/>
) : (
<p className={`align-center text-sm empty`}>
{t("admin.empty")}
</p>
)}
</div>
)}
</Droppable>
</DragDropContext>
<div className={`market-footer flex flex-row items-center mt-4`}>
<div className={`grow`} />
<Button
Expand Down
11 changes: 6 additions & 5 deletions globals/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ import (
const ChatMaxThread = 5
const AnonymousMaxThread = 1

var AllowedOrigins = []string{
"chatnio.net",
"fystart.cn",
"fystart.com",
}
var AllowedOrigins []string

var NotifyUrl = ""
var ArticlePermissionGroup []string
Expand All @@ -23,6 +19,11 @@ var CacheAcceptedExpire int64
var CacheAcceptedSize int64

func OriginIsAllowed(uri string) bool {
if len(AllowedOrigins) == 0 {
// if allowed origins is empty, allow all origins
return true
}

instance, _ := url.Parse(uri)
if instance == nil {
return false
Expand Down

0 comments on commit dcc2572

Please sign in to comment.