feat: update plan config and websocket connection deadline feature

This commit is contained in:
Zhang Minghan 2024-01-16 18:12:20 +08:00
parent 7f1c8fad99
commit 5dc965992a
54 changed files with 622 additions and 412 deletions

View File

@ -39,7 +39,7 @@
"cmdk": "^0.2.0",
"i18next": "^23.4.6",
"localforage": "^1.10.0",
"lucide-react": "^0.289.0",
"lucide-react": "^0.309.0",
"match-sorter": "^6.3.1",
"next-themes": "^0.2.1",
"react": "^18.2.0",

129
app/pnpm-lock.yaml generated
View File

@ -87,8 +87,8 @@ dependencies:
specifier: ^1.10.0
version: 1.10.0
lucide-react:
specifier: ^0.289.0
version: 0.289.0(react@18.2.0)
specifier: ^0.309.0
version: 0.309.0(react@18.2.0)
match-sorter:
specifier: ^6.3.1
version: 6.3.1
@ -548,6 +548,13 @@ packages:
'@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15
/@jridgewell/trace-mapping@0.3.21:
resolution: {integrity: sha512-SRfKmRe1KvYnxjEMtxEr+J4HIeMX5YBg/qhRHpxEIGjhX1rshcHlnFUE9K0GazhVKWM7B+nARSkV8LuvJdJ5/g==}
dependencies:
'@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15
dev: true
/@kurkle/color@0.3.2:
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
dev: false
@ -2092,12 +2099,12 @@ packages:
/@types/eslint-scope@3.7.7:
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
dependencies:
'@types/eslint': 8.44.8
'@types/eslint': 8.56.2
'@types/estree': 1.0.5
dev: true
/@types/eslint@8.44.8:
resolution: {integrity: sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==}
/@types/eslint@8.56.2:
resolution: {integrity: sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==}
dependencies:
'@types/estree': 1.0.5
'@types/json-schema': 7.0.15
@ -2152,8 +2159,8 @@ packages:
resolution: {integrity: sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ==}
dev: false
/@types/node@20.10.1:
resolution: {integrity: sha512-T2qwhjWwGH81vUEx4EXmBKsTJRXFXNZTL4v0gi01+zyBmCwzE6TyHszqX01m+QHTEq+EZNo13NeJIdEqf+Myrg==}
/@types/node@20.11.3:
resolution: {integrity: sha512-nrlmbvGPNGaj84IJZXMPhQuCMEVTT/hXZMJJG/aIqVL9fKxqk814sGGtJA4GI6hpJSLQjpi6cn0Qx9eOf9SDVg==}
dependencies:
undici-types: 5.26.5
dev: true
@ -2483,12 +2490,12 @@ packages:
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
dev: true
/acorn-import-assertions@1.9.0(acorn@8.11.2):
/acorn-import-assertions@1.9.0(acorn@8.11.3):
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
peerDependencies:
acorn: ^8
dependencies:
acorn: 8.11.2
acorn: 8.11.3
dev: true
/acorn-jsx@5.3.2(acorn@8.10.0):
@ -2505,8 +2512,8 @@ packages:
hasBin: true
dev: true
/acorn@8.11.2:
resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==}
/acorn@8.11.3:
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
engines: {node: '>=0.4.0'}
hasBin: true
dev: true
@ -2647,6 +2654,17 @@ packages:
update-browserslist-db: 1.0.13(browserslist@4.22.1)
dev: true
/browserslist@4.22.2:
resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
caniuse-lite: 1.0.30001576
electron-to-chromium: 1.4.631
node-releases: 2.0.14
update-browserslist-db: 1.0.13(browserslist@4.22.2)
dev: true
/buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: true
@ -2676,6 +2694,10 @@ packages:
/caniuse-lite@1.0.30001554:
resolution: {integrity: sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ==}
dev: true
/caniuse-lite@1.0.30001576:
resolution: {integrity: sha512-ff5BdakGe2P3SQsMsiqmt1Lc8221NR1VzHj5jXN5vBny9A6fpze94HiVV/n7XRosOlsShJcvMv5mdnpjOGCEgg==}
/ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@ -2869,19 +2891,6 @@ packages:
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
/debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
requiresBuild: true
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.1.3
dev: true
optional: true
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'}
@ -3004,6 +3013,10 @@ packages:
resolution: {integrity: sha512-8KR114CAYQ4/r5EIEsOmOMqQ9j0MRbJZR3aXD/KFA8RuKzyoUB4XrUCg+l8RUGqTVQgKNIgTpjaG8YHRPAbX2w==}
dev: true
/electron-to-chromium@1.4.631:
resolution: {integrity: sha512-g73CJB/rMPjdxpiNJYmV1homV7mLVUNe/R0z/HhqMfpjkt58FpYmkTjbtuv3zymdbTTJ+VOEqe1c+lkTjSOhmQ==}
dev: true
/enhanced-resolve@5.15.0:
resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==}
engines: {node: '>=10.13.0'}
@ -3689,7 +3702,7 @@ packages:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 20.10.1
'@types/node': 20.11.3
merge-stream: 2.0.0
supports-color: 8.1.1
dev: true
@ -3776,10 +3789,8 @@ packages:
image-size: 0.5.5
make-dir: 2.1.0
mime: 1.6.0
needle: 3.2.0
needle: 3.3.1
source-map: 0.6.1
transitivePeerDependencies:
- supports-color
dev: true
/levn@0.4.1:
@ -3856,8 +3867,8 @@ packages:
yallist: 4.0.0
dev: true
/lucide-react@0.289.0(react@18.2.0):
resolution: {integrity: sha512-D3/kt5h4KVmO9Bqlhky/szWI3puEU/KJfQWCeX8Zhvx3xx0SQ4t6vbwiK9ORBbiaqXefkBbXjoq7fOBd7s5yXQ==}
/lucide-react@0.309.0(react@18.2.0):
resolution: {integrity: sha512-zNVPczuwFrCfksZH3zbd1UDE6/WYhYAdbe2k7CImVyPAkXLgIwbs6eXQ4loigqDnUFjyFYCI5jZ1y10Kqal0dg==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
dependencies:
@ -4356,12 +4367,6 @@ packages:
/ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
requiresBuild: true
dev: true
optional: true
/mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
dependencies:
@ -4378,17 +4383,14 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
/needle@3.2.0:
resolution: {integrity: sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==}
/needle@3.3.1:
resolution: {integrity: sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==}
engines: {node: '>= 4.4.x'}
hasBin: true
requiresBuild: true
dependencies:
debug: 3.2.7
iconv-lite: 0.6.3
sax: 1.3.0
transitivePeerDependencies:
- supports-color
dev: true
optional: true
@ -4426,7 +4428,7 @@ packages:
'@next/env': 14.0.4
'@swc/helpers': 0.5.2
busboy: 1.6.0
caniuse-lite: 1.0.30001554
caniuse-lite: 1.0.30001576
graceful-fs: 4.2.11
postcss: 8.4.31
react: 18.2.0
@ -4466,6 +4468,10 @@ packages:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
dev: true
/node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
dev: true
/normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@ -5212,8 +5218,8 @@ packages:
lru-cache: 6.0.0
dev: true
/serialize-javascript@6.0.1:
resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==}
/serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
dependencies:
randombytes: 2.1.0
dev: true
@ -5393,8 +5399,8 @@ packages:
engines: {node: '>=6'}
dev: true
/terser-webpack-plugin@5.3.9(webpack@5.89.0):
resolution: {integrity: sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==}
/terser-webpack-plugin@5.3.10(webpack@5.89.0):
resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==}
engines: {node: '>= 10.13.0'}
peerDependencies:
'@swc/core': '*'
@ -5409,11 +5415,11 @@ packages:
uglify-js:
optional: true
dependencies:
'@jridgewell/trace-mapping': 0.3.20
'@jridgewell/trace-mapping': 0.3.21
jest-worker: 27.5.1
schema-utils: 3.3.0
serialize-javascript: 6.0.1
terser: 5.24.0
serialize-javascript: 6.0.2
terser: 5.26.0
webpack: 5.89.0
dev: true
@ -5428,13 +5434,13 @@ packages:
source-map-support: 0.5.21
dev: true
/terser@5.24.0:
resolution: {integrity: sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==}
/terser@5.26.0:
resolution: {integrity: sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==}
engines: {node: '>=10'}
hasBin: true
dependencies:
'@jridgewell/source-map': 0.3.5
acorn: 8.11.2
acorn: 8.11.3
commander: 2.20.3
source-map-support: 0.5.21
dev: true
@ -5619,6 +5625,17 @@ packages:
picocolors: 1.0.0
dev: true
/update-browserslist-db@1.0.13(browserslist@4.22.2):
resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
dependencies:
browserslist: 4.22.2
escalade: 3.1.1
picocolors: 1.0.0
dev: true
/uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
dependencies:
@ -5817,9 +5834,9 @@ packages:
'@webassemblyjs/ast': 1.11.6
'@webassemblyjs/wasm-edit': 1.11.6
'@webassemblyjs/wasm-parser': 1.11.6
acorn: 8.11.2
acorn-import-assertions: 1.9.0(acorn@8.11.2)
browserslist: 4.22.1
acorn: 8.11.3
acorn-import-assertions: 1.9.0(acorn@8.11.3)
browserslist: 4.22.2
chrome-trace-event: 1.0.3
enhanced-resolve: 5.15.0
es-module-lexer: 1.4.1
@ -5833,7 +5850,7 @@ packages:
neo-async: 2.6.2
schema-utils: 3.3.0
tapable: 2.2.1
terser-webpack-plugin: 5.3.9(webpack@5.89.0)
terser-webpack-plugin: 5.3.10(webpack@5.89.0)
watchpack: 2.4.0
webpack-sources: 3.2.3
transitivePeerDependencies:

View File

@ -4,7 +4,7 @@ import {
setAppName,
setBlobEndpoint,
setDocsUrl,
} from "@/utils/env.ts";
} from "@/conf/env.ts";
export type SiteInfo = {
title: string;

View File

@ -1,7 +1,7 @@
import { tokenField, ws_api } from "@/conf.ts";
import { tokenField, websocketEndpoint } from "@/conf";
import { getMemory } from "@/utils/memory.ts";
export const endpoint = `${ws_api}/chat`;
export const endpoint = `${websocketEndpoint}/chat`;
export type StreamMessage = {
conversation?: number;

View File

@ -1,5 +1,5 @@
import axios from "axios";
import { blobEndpoint } from "@/utils/env.ts";
import { blobEndpoint } from "@/conf/env.ts";
export type BlobParserResponse = {
status: boolean;

View File

@ -1,7 +1,7 @@
import { tokenField, ws_api } from "@/conf.ts";
import { tokenField, websocketEndpoint } from "@/conf";
import { getMemory } from "@/utils/memory.ts";
export const endpoint = `${ws_api}/generation/create`;
export const endpoint = `${websocketEndpoint}/generation/create`;
export type GenerationForm = {
token: string;

View File

@ -42,7 +42,7 @@ export type ConversationMapper = Record<Id, Conversation>;
export type PlanItem = {
id: string;
name: string;
value: string;
value: number;
icon: string;
models: string[];
};
@ -53,6 +53,8 @@ export type Plan = {
items: PlanItem[];
};
export type Plans = Plan[];
export type SubscriptionUsage = Record<
string,
{

View File

@ -110,6 +110,9 @@
}
.model-name {
display: flex;
flex-direction: row;
align-items: center;
color: hsl(var(--text));
}
@ -200,13 +203,32 @@
}
}
.market-tip {
transform: translateY(1px);
}
.model-name {
display: flex;
flex-direction: row;
align-items: center;
.badge {
transform: translateY(-2px);
}
&.pro {
p {
// 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;
}
.badge {
color: rgb(164, 128, 0) !important;
background: rgb(255, 231, 145) !important;
}
}
}
.model-tag {

View File

@ -83,14 +83,6 @@
border-color: hsl(var(--text-secondary));
}
&.enterprise {
border-color: hsl(var(--accent));
.desc {
margin-top: 12px;
}
}
.title {
text-align: center;
font-size: 16px;
@ -157,6 +149,12 @@
gap: 6px;
margin: 24px 0;
.api-tip {
display: flex;
flex-direction: column;
align-items: center;
}
div {
display: flex;
flex-direction: row;

View File

@ -1,4 +1,4 @@
import { deeptrainApiEndpoint, useDeeptrain } from "@/utils/env.ts";
import { deeptrainApiEndpoint, useDeeptrain } from "@/conf/env.ts";
import { ImgHTMLAttributes, useMemo } from "react";
import { cn } from "@/components/ui/lib/utils.ts";

View File

@ -1,7 +1,7 @@
import React from "react";
import { AlertCircle, Download } from "lucide-react";
import { withTranslation, WithTranslation } from "react-i18next";
import { version } from "@/conf.ts";
import { version } from "@/conf";
import { getMemoryPerformance } from "@/utils/app.ts";
import { Button } from "@/components/ui/button.tsx";
import { saveAsFile } from "@/utils/dom.ts";

View File

@ -24,7 +24,7 @@ import { useDraggableInput } from "@/utils/dom.ts";
import { FileObject, FileArray, blobParser } from "@/api/file.ts";
import { Button } from "@/components/ui/button.tsx";
import { useSelector } from "react-redux";
import { isHighContextModel } from "@/conf.ts";
import { isHighContextModel } from "@/conf/model.ts";
import { selectModel } from "@/store/chat.ts";
import { ChatAction } from "@/components/home/assemblies/ChatAction.tsx";
import { cn } from "@/components/ui/lib/utils.ts";

View File

@ -1,4 +1,4 @@
import { version } from "@/conf.ts";
import { version } from "@/conf";
import { useTranslation } from "react-i18next";
import { useToast } from "./ui/use-toast.ts";
import { getMemory, setMemory } from "@/utils/memory.ts";

View File

@ -5,21 +5,25 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip.tsx";
import { HelpCircle } from "lucide-react";
import React from "react";
import { cn } from "@/components/ui/lib/utils.ts";
type TipsProps = {
content: string;
content?: string;
children?: React.ReactNode;
className?: string;
};
function Tips({ content, className }: TipsProps) {
function Tips({ content, children, className }: TipsProps) {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className={`tips-icon ${className}`} />
<HelpCircle className={cn("tips-icon", className)} />
</TooltipTrigger>
<TooltipContent>
<p>{content}</p>
{content && <p>{content}</p>}
{children}
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@ -55,7 +55,7 @@ import { useToast } from "@/components/ui/use-toast";
import { deleteCharge, listCharge, setCharge } from "@/admin/api/charge.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { cn } from "@/components/ui/lib/utils.ts";
import { allModels } from "@/conf.ts";
import { allModels } from "@/conf";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
import Tips from "@/components/Tips.tsx";

View File

@ -3,7 +3,7 @@ import { ThemeProvider } from "@/components/ThemeProvider.tsx";
import DialogManager from "@/dialogs";
import Broadcast from "@/components/Broadcast.tsx";
import { useEffectAsync } from "@/utils/hook.ts";
import { allModels, supportModels } from "@/conf.ts";
import { allModels, supportModels } from "@/conf";
import { channelModels } from "@/admin/channel.ts";
import { getApiCharge, getApiMarket, getApiModels } from "@/api/v1.ts";
import { loadPreferenceModels } from "@/utils/storage.ts";

View File

@ -27,7 +27,7 @@ import { openDialog as openInvitationDialog } from "@/store/invitation.ts";
import { openDialog as openSharingDialog } from "@/store/sharing.ts";
import { openDialog as openApiDialog } from "@/store/api.ts";
import router from "@/router.tsx";
import { useDeeptrain } from "@/utils/env.ts";
import { useDeeptrain } from "@/conf/env.ts";
import React from "react";
type MenuBarProps = {

View File

@ -9,7 +9,7 @@ import {
import { Button } from "@/components/ui/button.tsx";
import { Menu } from "lucide-react";
import { useEffect } from "react";
import { tokenField } from "@/conf.ts";
import { tokenField } from "@/conf";
import { toggleMenu } from "@/store/menu.ts";
import ProjectLink from "@/components/ProjectLink.tsx";
import ModeToggle from "@/components/ThemeProvider.tsx";
@ -18,7 +18,7 @@ import MenuBar from "./MenuBar.tsx";
import { getMemory } from "@/utils/memory.ts";
import { goAuth } from "@/utils/app.ts";
import Avatar from "@/components/Avatar.tsx";
import { appLogo } from "@/utils/env.ts";
import { appLogo } from "@/conf/env.ts";
function NavMenu() {
const username = useSelector(selectUsername);

View File

@ -20,7 +20,7 @@ import {
} from "@/components/ui/dialog.tsx";
import { getLanguage } from "@/i18n.ts";
import { selectAuthenticated } from "@/store/auth.ts";
import { docsEndpoint, useDeeptrain } from "@/utils/env.ts";
import { docsEndpoint, useDeeptrain } from "@/conf/env.ts";
function ChatSpace() {
const [open, setOpen] = useState(false);

View File

@ -1,7 +1,6 @@
import SelectGroup, { SelectItemProps } from "@/components/SelectGroup.tsx";
import { supportModels } from "@/conf.ts";
import { supportModels } from "@/conf";
import {
getPlanModels,
openMarket,
selectModel,
selectModelList,
@ -19,6 +18,7 @@ import { ToastAction } from "@/components/ui/toast.tsx";
import { useEffect, useMemo, useState } from "react";
import { Sparkles } from "lucide-react";
import { goAuth } from "@/utils/app.ts";
import { includingModelFromPlan } from "@/conf/subscription.tsx";
function GetModel(name: string): Model {
return supportModels.find((model) => model.id === name) as Model;
@ -29,7 +29,7 @@ type ModelSelectorProps = {
};
function filterModel(model: Model, level: number) {
if (getPlanModels(level).includes(model.id)) {
if (includingModelFromPlan(level, model.id)) {
return {
name: model.id,
value: model.name,

View File

@ -11,14 +11,13 @@ import {
X,
} from "lucide-react";
import React, { useMemo, useState } from "react";
import { supportModels } from "@/conf.ts";
import { supportModels } from "@/conf";
import { isUrl, splitList } from "@/utils/base.ts";
import { Model } from "@/api/types.ts";
import { useDispatch, useSelector } from "react-redux";
import {
addModelList,
closeMarket,
getPlanModels,
removeModelList,
selectModel,
selectModelList,
@ -30,7 +29,7 @@ import { teenagerSelector } from "@/store/package.ts";
import { ToastAction } from "@/components/ui/toast.tsx";
import { selectAuthenticated } from "@/store/auth.ts";
import { useToast } from "@/components/ui/use-toast.ts";
import { docsEndpoint } from "@/utils/env.ts";
import { docsEndpoint } from "@/conf/env.ts";
import { goAuth } from "@/utils/app.ts";
import {
DragDropContext,
@ -40,6 +39,16 @@ import {
} from "react-beautiful-dnd";
import { savePreferenceModels } from "@/utils/storage.ts";
import { cn } from "@/components/ui/lib/utils.ts";
import { Badge } from "@/components/ui/badge.tsx";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip.tsx";
import { useMobile } from "@/utils/device.ts";
import Tips from "@/components/Tips.tsx";
import { includingModelFromPlan } from "@/conf/subscription.tsx";
type SearchBarProps = {
value: string;
@ -100,6 +109,8 @@ function ModelItem({
const list = useSelector(selectModelList);
const current = useSelector(selectModel);
const mobile = useMobile();
const level = useSelector(levelSelector);
const student = useSelector(teenagerSelector);
const auth = useSelector(selectAuthenticated);
@ -111,7 +122,7 @@ function ModelItem({
}, [model, current, list]);
const pro = useMemo(() => {
return getPlanModels(level).includes(model.id);
return includingModelFromPlan(level, model.id);
}, [model, level, student]);
const avatar = useMemo(() => {
@ -148,7 +159,32 @@ function ModelItem({
<GripVertical className={`grip-icon h-4 w-4 translate-x-[-1rem]`} />
<img className={`model-avatar`} src={avatar} alt={model.name} />
<div className={`model-info`}>
<p className={cn("model-name", pro && "pro")}>{model.name}</p>
<div className={cn("model-name", pro && "pro")}>
<p>{model.name}</p>
{mobile ? (
<Tips className={`market-tip`}>
<div className={`flex flex-col items-center justify-center`}>
<p>{t("market.model-api")}</p>
<Badge className={`badge whitespace-nowrap mt-2`}>
{model.id}
</Badge>
</div>
</Tips>
) : (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<Badge
className={`badge whitespace-nowrap inline-block ml-2`}
>
{model.id}
</Badge>
</TooltipTrigger>
<TooltipContent>{t("market.model-api")}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
{model.description && (
<p className={`model-description`}>{model.description}</p>
)}

View File

@ -23,37 +23,38 @@ import { DialogClose } from "@radix-ui/react-dialog";
import { Button } from "@/components/ui/button.tsx";
import { expiredSelector, refreshSubscription } from "@/store/subscription.ts";
import { Plus } from "lucide-react";
import { subscriptionPrize } from "@/conf.ts";
import { ToastAction } from "@/components/ui/toast.tsx";
import { deeptrainEndpoint, useDeeptrain } from "@/utils/env.ts";
import { deeptrainEndpoint, useDeeptrain } from "@/conf/env.ts";
import { AppDispatch } from "@/store";
import { openDialog } from "@/store/quota.ts";
import { getPlanPrice } from "@/conf/subscription.tsx";
function countPrize(base: number, month: number): number {
const prize = subscriptionPrize[base] * month;
function countPrice(base: number, month: number): number {
const price = getPlanPrice(base) * month;
if (month >= 36) {
return prize * 0.7;
return price * 0.7;
} else if (month >= 12) {
return prize * 0.8;
return price * 0.8;
} else if (month >= 6) {
return prize * 0.9;
return price * 0.9;
}
return prize;
return price;
}
function countUpgradePrize(
function countUpgradePrice(
level: number,
target: number,
days: number,
): number {
const bias = subscriptionPrize[target] - subscriptionPrize[level];
return (bias / 30) * days;
const bias = getPlanPrice(target) - getPlanPrice(level);
const v = (bias / 30) * days;
return v > 0 ? v + 1 : 0; // time count offset
}
type UpgradeProps = {
base: number;
level: number;
current: number;
};
async function callBuyAction(
@ -118,7 +119,7 @@ async function callMigrateAction(
return res.status;
}
export function Upgrade({ base, level }: UpgradeProps) {
export function Upgrade({ level, current }: UpgradeProps) {
const { t } = useTranslation();
const expired = useSelector(expiredSelector);
const [open, setOpen] = React.useState(false);
@ -126,10 +127,10 @@ export function Upgrade({ base, level }: UpgradeProps) {
const dispatch = useDispatch();
const { toast } = useToast();
const isCurrent = useMemo(() => level === base, [level, base]);
const isUpgrade = useMemo(() => level < base, [level, base]);
const isCurrent = useMemo(() => current === level, [current, level]);
const isUpgrade = useMemo(() => current < level, [current, level]);
return level === 0 || level === base ? (
return current === 0 || current === level ? (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className={`action`} variant={`default`}>
@ -169,13 +170,13 @@ export function Upgrade({ base, level }: UpgradeProps) {
</SelectContent>
</Select>
<p className={`price`}>
{t("sub.price", { price: countPrize(base, month).toFixed(2) })}
{t("sub.price", { price: countPrice(level, month).toFixed(2) })}
{useDeeptrain && (
<span className={`tax`}>
&nbsp; (
{t("sub.price-tax", {
price: (countPrize(base, month) * 0.25).toFixed(1),
price: (countPrice(level, month) * 0.25).toFixed(1),
})}
)
</span>
@ -189,7 +190,7 @@ export function Upgrade({ base, level }: UpgradeProps) {
<Button
className={`mb-1.5`}
onClick={async () => {
const res = await callBuyAction(t, toast, dispatch, month, base);
const res = await callBuyAction(t, toast, dispatch, month, level);
if (res) {
setOpen(false);
await refreshSubscription(dispatch);
@ -222,7 +223,7 @@ export function Upgrade({ base, level }: UpgradeProps) {
{isUpgrade && (
<p className={`price`}>
{t("sub.upgrade-price", {
price: countUpgradePrize(level, base, expired).toFixed(2),
price: countUpgradePrice(current, level, expired).toFixed(2),
})}
</p>
)}
@ -234,7 +235,7 @@ export function Upgrade({ base, level }: UpgradeProps) {
<Button
className={`mb-1.5`}
onClick={async () => {
const res = await callMigrateAction(t, toast, base);
const res = await callMigrateAction(t, toast, level);
if (res) {
setOpen(false);
await refreshSubscription(dispatch);

View File

@ -1,8 +1,9 @@
import { SubscriptionIcon } from "@/conf/subscription.tsx";
import React from "react";
import Icon from "@/components/utils/Icon.tsx";
type UsageProps = {
icon: React.ReactElement;
icon: string | React.ReactElement;
name: string;
usage:
| {
@ -17,7 +18,11 @@ function SubscriptionUsage({ icon, name, usage }: UsageProps) {
return (
usage && (
<div className={`sub-column`}>
{typeof icon === "string" ? (
<SubscriptionIcon type={icon} className={`h-4 w-4 mr-1`} />
) : (
<Icon icon={icon} className={`h-4 w-4 mr-1`} />
)}
{name}
<div className={`grow`} />
{typeof usage === "number" ? (

View File

@ -1,72 +0,0 @@
import axios from "axios";
import { Model, PlanModel, SubscriptionUsage } from "@/api/types.ts";
import {
deeptrainAppName,
deeptrainEndpoint,
getDev,
getRestApi,
getTokenField,
getWebsocketApi,
} from "@/utils/env.ts";
import { getMemory } from "@/utils/memory.ts";
import { Compass, Image, Newspaper } from "lucide-react";
import React from "react";
import { syncSiteInfo } from "@/admin/api/info.ts";
import { getOfflineModels, loadPreferenceModels } from "@/utils/storage.ts";
export const version = "3.8.6";
export const dev: boolean = getDev();
export const deploy: boolean = true;
export let rest_api: string = getRestApi(deploy);
export let ws_api: string = getWebsocketApi(deploy);
export const tokenField = getTokenField(deploy);
export let supportModels: Model[] = loadPreferenceModels(getOfflineModels());
export let allModels: string[] = supportModels.map((model) => model.id);
export const planModels: PlanModel[] = [
{ id: "gpt-4-0613", level: 1 },
{ id: "gpt-4-1106-preview", level: 1 },
{ id: "gpt-4-vision-preview", level: 1 },
{ id: "gpt-4-v", level: 1 },
{ id: "gpt-4-all", level: 1 },
{ id: "gpt-4-dalle", level: 1 },
{ id: "claude-2", level: 1 },
{ id: "claude-2.1", level: 1 },
{ id: "claude-2-100k", level: 1 },
{ id: "midjourney-fast", level: 1 },
];
export const subscriptionPrize: Record<number, number> = {
1: 42,
2: 76,
3: 148,
};
export const subscriptionUsage: SubscriptionUsage = {
midjourney: { name: "Midjourney", icon: React.createElement(Image) },
"gpt-4": { name: "GPT-4", icon: React.createElement(Compass) },
"claude-100k": { name: "Claude 100k", icon: React.createElement(Newspaper) },
};
export function getModelFromId(id: string): Model | undefined {
return supportModels.find((model) => model.id === id);
}
export function isHighContextModel(id: string): boolean {
const model = getModelFromId(id);
return !!model && model.high_context;
}
export function login() {
location.href = `${deeptrainEndpoint}/login?app=${
dev ? "dev" : deeptrainAppName
}`;
}
axios.defaults.baseURL = rest_api;
axios.defaults.headers.post["Content-Type"] = "application/json";
axios.defaults.headers.common["Authorization"] = getMemory(tokenField);
syncSiteInfo();

13
app/src/conf/api.ts Normal file
View File

@ -0,0 +1,13 @@
import axios from "axios";
import { getMemory } from "@/utils/memory.ts";
type AxiosConfig = {
endpoint: string;
token: string;
};
export function setAxiosConfig(config: AxiosConfig) {
axios.defaults.baseURL = config.endpoint;
axios.defaults.headers.post["Content-Type"] = "application/json";
axios.defaults.headers.common["Authorization"] = getMemory(config.token);
}

View File

@ -0,0 +1,8 @@
import { deeptrainAppName, deeptrainEndpoint } from "@/conf/env.ts";
import { dev } from "@/conf/index.ts";
export function goDeepLogin() {
location.href = `${deeptrainEndpoint}/login?app=${
dev ? "dev" : deeptrainAppName
}`;
}

125
app/src/conf/index.ts Normal file
View File

@ -0,0 +1,125 @@
import { Model, PlanModel, Plans } from "@/api/types.ts";
import {
getDev,
getRestApi,
getTokenField,
getWebsocketApi,
} from "@/conf/env.ts";
import { syncSiteInfo } from "@/admin/api/info.ts";
import { getOfflineModels, loadPreferenceModels } from "@/utils/storage.ts";
import { setAxiosConfig } from "@/conf/api.ts";
export const version = "3.8.6"; // version of the current build
export const dev: boolean = getDev(); // is in development mode (for debugging, in localhost origin)
export const deploy: boolean = true; // is production environment (for api endpoint)
export let apiEndpoint: string = getRestApi(deploy); // api endpoint for rest api calls
export let websocketEndpoint: string = getWebsocketApi(deploy); // api endpoint for websocket calls
export const tokenField = getTokenField(deploy); // token field name for storing token
export let supportModels: Model[] = loadPreferenceModels(getOfflineModels()); // support models in model market of the current site
export let allModels: string[] = supportModels.map((model) => model.id); // all support model id list of the current site
const GPT4Array = [
"gpt-4",
"gpt-4-0314",
"gpt-4-0613",
"gpt-4-1106-preview",
"gpt-4-vision-preview",
"gpt-4-v",
"gpt-4-dalle",
"gpt-4-all",
];
const Claude100kArray = ["claude-1.3", "claude-2", "claude-2.1"];
const MidjourneyArray = ["midjourney-fast"];
export const subscriptionData: Plans = [
{
level: 1,
price: 42,
items: [
{
id: "gpt-4",
icon: "compass",
name: "GPT-4",
value: 150,
models: GPT4Array,
},
{
id: "midjourney",
icon: "image-plus",
name: "Midjourney",
value: 50,
models: MidjourneyArray,
},
{
id: "claude-100k",
icon: "book-text",
name: "Claude 100k",
value: 300,
models: Claude100kArray,
},
],
},
{
level: 2,
price: 76,
items: [
{
id: "gpt-4",
icon: "compass",
name: "GPT-4",
value: 300,
models: GPT4Array,
},
{
id: "midjourney",
icon: "image-plus",
name: "Midjourney",
value: 100,
models: MidjourneyArray,
},
{
id: "claude-100k",
icon: "book-text",
name: "Claude 100k",
value: 600,
models: Claude100kArray,
},
],
},
{
level: 3,
price: 148,
items: [
{
id: "gpt-4",
icon: "compass",
name: "GPT-4",
value: 600,
models: GPT4Array,
},
{
id: "midjourney",
icon: "image-plus",
name: "Midjourney",
value: 200,
models: MidjourneyArray,
},
{
id: "claude-100k",
icon: "book-text",
name: "Claude 100k",
value: 1200,
models: Claude100kArray,
},
],
},
];
setAxiosConfig({
endpoint: apiEndpoint,
token: tokenField,
});
syncSiteInfo();

11
app/src/conf/model.ts Normal file
View File

@ -0,0 +1,11 @@
import { Model } from "@/api/types.ts";
import { supportModels } from "@/conf/index.ts";
export function getModelFromId(id: string): Model | undefined {
return supportModels.find((model) => model.id === id);
}
export function isHighContextModel(id: string): boolean {
const model = getModelFromId(id);
return !!model && model.high_context;
}

View File

@ -0,0 +1,61 @@
import {
BookText,
Compass,
Image,
ImagePlus,
Video,
AudioLines,
} from "lucide-react";
import React, { useMemo } from "react";
import { subscriptionData } from "@/conf/index.ts";
import { Plan } from "@/api/types.ts";
import Icon from "@/components/utils/Icon.tsx";
export const subscriptionIcons: Record<string, React.ReactElement> = {
compass: <Compass />,
image: <Image />,
imageplus: <ImagePlus />,
booktext: <BookText />,
video: <Video />,
audio: <AudioLines />,
};
export const subscriptionType: Record<number, string> = {
1: "basic",
2: "standard",
3: "pro",
};
type SubscriptionIconProps = {
type: string;
className?: string;
};
export function SubscriptionIcon({ type, className }: SubscriptionIconProps) {
const icon = useMemo(() => {
return subscriptionIcons[type.toLowerCase()] || subscriptionIcons.compass;
}, [type]);
return <Icon icon={icon} className={className} />;
}
export function getPlan(level: number): Plan {
const raw = subscriptionData.filter((item) => item.level === level);
return raw.length > 0 ? raw[0] : subscriptionData[0];
}
export function getPlanModels(level: number): string[] {
return getPlan(level).items.flatMap((item) => item.models);
}
export function includingModelFromPlan(level: number, model: string): boolean {
return getPlanModels(level).includes(model);
}
export function getPlanPrice(level: number): number {
return getPlan(level).price;
}
export function getPlanName(level: number): string {
return subscriptionType[level] || "none";
}

View File

@ -24,7 +24,7 @@ import { useToast } from "@/components/ui/use-toast.ts";
import { copyClipboard } from "@/utils/dom.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { selectInit } from "@/store/auth.ts";
import { docsEndpoint } from "@/utils/env.ts";
import { docsEndpoint } from "@/conf/env.ts";
import {
AlertDialog,
AlertDialogCancel,

View File

@ -23,7 +23,7 @@ import { Separator } from "@/components/ui/separator.tsx";
import { Badge } from "@/components/ui/badge.tsx";
import { useEffectAsync } from "@/utils/hook.ts";
import { selectAuthenticated } from "@/store/auth.ts";
import { deeptrainEndpoint, useDeeptrain } from "@/utils/env.ts";
import { deeptrainEndpoint, useDeeptrain } from "@/conf/env.ts";
function PackageDialog() {
const { t } = useTranslation();

View File

@ -39,7 +39,7 @@ import { useToast } from "@/components/ui/use-toast.ts";
import { useEffectAsync } from "@/utils/hook.ts";
import { selectAuthenticated } from "@/store/auth.ts";
import { ToastAction } from "@/components/ui/toast.tsx";
import { deeptrainEndpoint, docsEndpoint, useDeeptrain } from "@/utils/env.ts";
import { deeptrainEndpoint, docsEndpoint, useDeeptrain } from "@/conf/env.ts";
import { useRedeem } from "@/api/redeem.ts";
import { cn } from "@/components/ui/lib/utils.ts";

View File

@ -24,7 +24,7 @@ import {
import { Checkbox } from "@/components/ui/checkbox.tsx";
import { useEffect, useState } from "react";
import { getMemoryPerformance } from "@/utils/app.ts";
import { version } from "@/conf.ts";
import { version } from "@/conf";
import { NumberInput } from "@/components/ui/number-input.tsx";
import {
Select,

View File

@ -22,22 +22,68 @@ import {
openDialog as openQuotaDialog,
dialogSelector as quotaDialogSelector,
} from "@/store/quota.ts";
import {
Award,
BookText,
Calendar,
Compass,
ImagePlus,
LifeBuoy,
ServerCrash,
} from "lucide-react";
import { Calendar } from "lucide-react";
import { useEffectAsync } from "@/utils/hook.ts";
import { selectAuthenticated } from "@/store/auth.ts";
import SubscriptionUsage from "@/components/home/subscription/SubscriptionUsage.tsx";
import Tips from "@/components/Tips.tsx";
import { subscriptionPrize, subscriptionUsage } from "@/conf.ts";
import { Upgrade } from "@/components/home/subscription/BuyDialog.tsx";
import { useDeeptrain } from "@/utils/env.ts";
import { useDeeptrain } from "@/conf/env.ts";
import { useMemo } from "react";
import {
getPlan,
getPlanName,
SubscriptionIcon,
} from "@/conf/subscription.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
import { subscriptionData } from "@/conf";
import { Badge } from "@/components/ui/badge.tsx";
type PlanItemProps = {
level: number;
};
function PlanItem({ level }: PlanItemProps) {
const { t } = useTranslation();
const current = useSelector(levelSelector);
const plan = useMemo(() => getPlan(level), [level]);
const name = useMemo(() => getPlanName(level), [level]);
return (
<div className={cn("plan", name)}>
<div className={`title`}>{t(`sub.${name}`)}</div>
<div className={`price-wrapper`}>
<div className={`price`}>
{t("sub.plan-price", { money: plan.price })}
</div>
{useDeeptrain && <p className={`annotate`}>({t("sub.include-tax")})</p>}
</div>
<div className={`desc`}>
{plan.items.map((item, index) => (
<div key={index}>
<SubscriptionIcon type={item.icon} className={`h-4 w-4 mr-1`} />
{t("sub.plan-usage", { name: item.name, times: item.value })}
<Tips>
<div className={`api-tip`}>
<p>{t("sub.plan-tip")}</p>
<div
className={`flex flex-row gap-2 mt-2 flex-wrap justify-center items-center max-w-[40vw]`}
>
{item.models.map((model, index) => (
<Badge key={index} className={`whitespace-nowrap`}>
{model}
</Badge>
))}
</div>
</div>
</Tips>
</div>
))}
</div>
<Upgrade level={level} current={current} />
</div>
);
}
function SubscriptionDialog() {
const { t } = useTranslation();
@ -49,6 +95,8 @@ function SubscriptionDialog() {
const auth = useSelector(selectAuthenticated);
const quota = useSelector(quotaDialogSelector);
const plan = useMemo(() => getPlan(level), [level]);
const dispatch = useDispatch();
useEffectAsync(async () => {
if (!auth) return;
@ -84,12 +132,13 @@ function SubscriptionDialog() {
usage={expired}
/>
{Object.entries(subscriptionUsage).map(
([key, props], index) =>
usage?.[key] && (
{plan.items.map(
(item, index) =>
usage?.[item.id] && (
<SubscriptionUsage
{...props}
usage={usage?.[key]}
name={item.name}
icon={item.icon}
usage={usage?.[item.id]}
key={index}
/>
),
@ -97,110 +146,9 @@ function SubscriptionDialog() {
</div>
)}
<div className={`plan-wrapper`}>
<div className={`plan basic`}>
<div className={`title`}>{t("sub.base")}</div>
<div className={`price-wrapper`}>
<div className={`price`}>
{t("sub.plan-price", { money: subscriptionPrize[1] })}
</div>
{useDeeptrain && (
<p className={`annotate`}>({t("sub.include-tax")})</p>
)}
</div>
<div className={`desc`}>
<div>
<Compass className={`h-4 w-4 mr-1`} />
{t("sub.plan-gpt4", { times: 150 })}
<Tips content={t("sub.plan-gpt4-desc")} />
</div>
<div>
<ImagePlus className={`h-4 w-4 mr-1`} />
{t("sub.plan-midjourney", { times: 50 })}
<Tips content={t("sub.plan-midjourney-desc")} />
</div>
<div>
<BookText className={`h-4 w-4 mr-1`} />
{t("sub.plan-claude", { times: 300 })}
<Tips content={t("sub.plan-claude-desc")} />
</div>
</div>
<Upgrade base={1} level={level} />
</div>
<div className={`plan standard`}>
<div className={`title`}>{t("sub.standard")}</div>
<div className={`price-wrapper`}>
<div className={`price`}>
{t("sub.plan-price", { money: subscriptionPrize[2] })}
</div>
{useDeeptrain && (
<p className={`annotate`}>({t("sub.include-tax")})</p>
)}
</div>
<div className={`desc`}>
<div>
<LifeBuoy className={`h-4 w-4 mr-1`} />
{t("sub.pro-service")}
</div>
<div>
<Compass className={`h-4 w-4 mr-1`} />
{t("sub.plan-gpt4", { times: 300 })}
<Tips content={t("sub.plan-gpt4-desc")} />
</div>
<div>
<ImagePlus className={`h-4 w-4 mr-1`} />
{t("sub.plan-midjourney", { times: 100 })}
<Tips content={t("sub.plan-midjourney-desc")} />
</div>
<div>
<BookText className={`h-4 w-4 mr-1`} />
{t("sub.plan-claude", { times: 600 })}
<Tips content={t("sub.plan-claude-desc")} />
</div>
</div>
<Upgrade base={2} level={level} />
</div>
<div className={`plan pro`}>
<div className={`title`}>{t("sub.pro")}</div>
<div className={`price-wrapper`}>
<div className={`price`}>
{t("sub.plan-price", { money: subscriptionPrize[3] })}
</div>
{useDeeptrain && (
<p className={`annotate`}>({t("sub.include-tax")})</p>
)}
</div>
<div className={`desc`}>
<div>
<ServerCrash className={`h-4 w-4 mr-1`} />
{t("sub.pro-thread")}
</div>
<div>
<Compass className={`h-4 w-4 mr-1`} />
{t("sub.plan-gpt4", { times: 600 })}
<Tips content={t("sub.plan-gpt4-desc")} />
</div>
<div>
<ImagePlus className={`h-4 w-4 mr-1`} />
{t("sub.plan-midjourney", { times: 200 })}
<Tips content={t("sub.plan-midjourney-desc")} />
</div>
<div>
<BookText className={`h-4 w-4 mr-1`} />
{t("sub.plan-claude", { times: 1200 })}
<Tips content={t("sub.plan-claude-desc")} />
</div>
</div>
<div className={`award`}>
<Award className={`h-3 w-3 mb-1`} />
<div className={`mb-1`}>
{t("sub.pro-award", {
content:
"Poe Pro ($20)\n x \nMidjourney Base Plan ($10)",
})}
</div>
</div>
<Upgrade base={3} level={level} />
</div>
{subscriptionData.map((item, index) => (
<PlanItem key={index} level={item.level} />
))}
</div>
</div>
</DialogDescription>

View File

@ -1,10 +1,10 @@
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./conf.ts";
import "./conf";
import "./i18n.ts";
import "./assets/main.less";
import "./assets/globals.less";
import "./conf.ts";
import "./conf";
import ReloadPrompt from "./components/ReloadService.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(

View File

@ -92,7 +92,8 @@
"title": "模型市场",
"model": "探索更多模型",
"explore": "探索",
"search": "搜索模型名称或者简介"
"search": "搜索模型名称或者简介",
"model-api": "API 请求的模型 ID 名称"
},
"conversation": {
"title": "对话",
@ -181,20 +182,8 @@
"pro": "专业版",
"plan-price": "{{money}} 元/月",
"include-tax": "含税",
"free-models": "免费模型永久免费",
"free-web": "联网搜索功能",
"free-conversation": "对话存储记录",
"free-sharing": "对话分享功能",
"free-api": "API 调用",
"plan-midjourney": "Midjourney 每月绘图 {{times}} 次",
"plan-midjourney-desc": "Midjourney 快速出图模式",
"plan-gpt4": "GPT-4 每月配额 {{times}} 次",
"plan-gpt4-desc": "包含 GPT 4 Turbo, GPT 4V, GPT 4 DALLE",
"plan-claude": "Claude 100k 每月配额 {{times}} 次",
"plan-claude-desc": "包含 Claude 2 (100k), Claude 2.1 (200k)",
"pro-service": "优先服务支持",
"pro-thread": "并发数提升",
"pro-award": "等价于 {{content}}",
"plan-usage": "{{name}} 每月使用 {{times}} 次",
"plan-tip": "可调用模型",
"enterprise": "企业版",
"enterprise-service": "优先技术支持",
"enterprise-sla": "SLA 保障",
@ -446,7 +435,7 @@
"retry-tip": "当渠道请求失败时,最多重试的次数",
"model": "模型",
"secret": "密钥",
"secret-placeholder": "请输入密钥,格式:{{format}}\n多个密钥时一行一个请求时随机选取负载",
"secret-placeholder": "请输入密钥,格式:{{format}} (<>不用填)\n多个密钥时一行一个请求时随机选取负载",
"endpoint": "接入点",
"endpoint-placeholder": "请输入接入点(即代理)",
"mapper": "模型映射",

View File

@ -141,20 +141,6 @@
"pro": "Pro",
"plan-price": "{{money}} CNY/Month",
"include-tax": "Include Tax",
"free-models": "Free Models Free Forever",
"free-web": "web searching feature",
"free-conversation": "conversation storage",
"free-sharing": "conversation sharing",
"free-api": "API calls",
"plan-midjourney": "Midjourney {{times}} image generation per month",
"plan-midjourney-desc": "Midjourney Quick Image Generation",
"plan-gpt4": "GPT-4 {{times}} requests per month",
"plan-gpt4-desc": "including GPT 4 Turbo, GPT 4V, GPT 4 DALLE",
"plan-claude": "Claude 100k {{times}} requests per month",
"plan-claude-desc": "including Claude 2 (100k), Claude 2.1 (200k)",
"pro-service": "Priority Service Support",
"pro-thread": "Concurrency Increase",
"pro-award": "Equivalent to {{content}}",
"enterprise": "Enterprise",
"enterprise-service": "Priority Service Support",
"enterprise-sla": "SLA Guarantee",
@ -190,7 +176,9 @@
"failed": "Subscribe failed",
"failed-prompt": "Failed to subscribe, please make sure you have enough balance.",
"migrate-failed": "Migrate failed",
"migrate-failed-prompt": "Your subscription migration failed."
"migrate-failed-prompt": "Your subscription migration failed.",
"plan-usage": "{{name}} uses {{times}} times per month",
"plan-tip": "Callable Model"
},
"cancel": "Cancel",
"confirm": "Confirm",
@ -372,7 +360,7 @@
"retry-tip": "When the channel request fails, the maximum number of retries",
"model": "Model",
"secret": "Secret",
"secret-placeholder": "Please enter the secret, format: {{format}}\nWhen there are multiple secrets, one line is selected randomly when requesting the load",
"secret-placeholder": "Please enter the secret, format: {{format}} (<> not filled)\nWhen there are multiple secrets, one line is selected randomly when requesting the load",
"endpoint": "Endpoint",
"endpoint-placeholder": "Please enter the endpoint (ie proxy)",
"mapper": "Model Mapper",

View File

@ -141,20 +141,6 @@
"pro": "プロ",
"plan-price": "{{money}}元/月",
"include-tax": "(税込)",
"free-models": "フリーモデルライフタイムフリー",
"free-web": "ネットワーク検索機能",
"free-conversation": "会話ストレージレコード",
"free-sharing": "会話の共有",
"free-api": "API呼び出し",
"plan-midjourney": "ミッドジャーニーは月に{{times}}回抽選されます",
"plan-midjourney-desc": "ミッドジャーニー・クイックプロットモード",
"plan-gpt4": "GPT -4ルマ{{times}}回/月",
"plan-gpt4-desc": "GPT 4 Turbo、GPT 4 V、GPT 4 DALLEを含む",
"plan-claude": "クロード10万回の月間ルマ{{times}}回",
"plan-claude-desc": "クロード2 100 k 、クロード2.1 200 k )が含まれています",
"pro-service": "優先サービスサポート",
"pro-thread": "同時実行数の増加",
"pro-award": "{{content}}相当",
"enterprise": "エンタープライズ版",
"enterprise-service": "優先技術サポート",
"enterprise-sla": "SLA保護",
@ -190,7 +176,9 @@
"failed": "サブスクリプションに失敗しました",
"failed-prompt": "サブスクリプションに失敗しました。十分な残高があることを確認してください。",
"migrate-failed": "変更できませんでした",
"migrate-failed-prompt": "サブスクリプションの変更に失敗しました。"
"migrate-failed-prompt": "サブスクリプションの変更に失敗しました。",
"plan-usage": "{{name}}は月に{{times}}回使用",
"plan-tip": "呼び出し可能なモデル"
},
"cancel": "キャンセル",
"confirm": "確認",

View File

@ -141,20 +141,6 @@
"pro": "Профессиональный",
"plan-price": "{{money}} CNY/месяц",
"include-tax": "Включая налог",
"free-models": "Бесплатные модели бесплатно навсегда",
"free-web": "веб-поиск",
"free-conversation": "хранение разговоров",
"free-sharing": "общий доступ к разговорам",
"free-api": "API вызовы",
"plan-midjourney": "Midjourney {{times}} генерация изображений в месяц",
"plan-midjourney-desc": "Быстрая генерация изображений Midjourney",
"plan-gpt4": "GPT-4 {{times}} запросов в месяц",
"plan-gpt4-desc": "включая GPT 4 Turbo, GPT 4V, GPT 4 DALLE",
"plan-claude": "Claude 100k {{times}} запросов в месяц",
"plan-claude-desc": "включая Claude 2 (100k), Claude 2.1 (200k)",
"pro-service": "Приоритетная служба поддержки",
"pro-thread": "Увеличение параллелизма",
"pro-award": "Эквивалент {{content}}",
"enterprise": "Корпоративный",
"enterprise-service": "Приоритетная служба поддержки",
"enterprise-sla": "Гарантия SLA",
@ -190,7 +176,9 @@
"failed": "Подписка не удалась",
"failed-prompt": "Не удалось подписаться, пожалуйста, убедитесь, что у вас достаточно баланса.",
"migrate-failed": "Перенос подписки не удался",
"migrate-failed-prompt": "Ваша подписка не удалась."
"migrate-failed-prompt": "Ваша подписка не удалась.",
"plan-usage": "{{name}} использует {{times}} раз в месяц",
"plan-tip": "Вызываемая модель"
},
"cancel": "Отмена",
"confirm": "Подтвердить",

View File

@ -8,7 +8,7 @@ import Home from "./routes/Home.tsx";
import NotFound from "./routes/NotFound.tsx";
import Auth from "./routes/Auth.tsx";
import React, { Suspense, useEffect } from "react";
import { useDeeptrain } from "@/utils/env.ts";
import { useDeeptrain } from "@/conf/env.ts";
import Register from "@/routes/Register.tsx";
import Forgot from "@/routes/Forgot.tsx";
import { lazyFactor } from "@/utils/loader.tsx";

View File

@ -17,7 +17,7 @@ import ModelFinder from "@/components/home/ModelFinder.tsx";
import { Toggle } from "@/components/ui/toggle.tsx";
import { selectModel, selectWeb, setWeb } from "@/store/chat.ts";
import { Label } from "@/components/ui/label.tsx";
import { rest_api, tokenField, ws_api } from "@/conf.ts";
import { apiEndpoint, tokenField, websocketEndpoint } from "@/conf";
import { getMemory } from "@/utils/memory.ts";
import { Progress } from "@/components/ui/progress.tsx";
import { cn } from "@/components/ui/lib/utils.ts";
@ -76,7 +76,7 @@ function ArticleContent() {
function generate() {
setProgress(true);
const connection = new WebSocket(`${ws_api}/article/create`);
const connection = new WebSocket(`${websocketEndpoint}/article/create`);
connection.onopen = () => {
connection.send(
@ -122,7 +122,7 @@ function ArticleContent() {
variant={`outline`}
className={`mt-5 w-full mr-2`}
onClick={() => {
location.href = `${rest_api}/article/download/zip?hash=${hash}`;
location.href = `${apiEndpoint}/article/download/zip?hash=${hash}`;
}}
>
{" "}
@ -133,7 +133,7 @@ function ArticleContent() {
variant={`outline`}
className={`mt-5 w-full ml-2`}
onClick={() => {
location.href = `${rest_api}/article/download/tar?hash=${hash}`;
location.href = `${apiEndpoint}/article/download/tar?hash=${hash}`;
}}
>
{" "}

View File

@ -1,6 +1,6 @@
import { useToast } from "@/components/ui/use-toast.ts";
import { ToastAction } from "@/components/ui/toast.tsx";
import { tokenField } from "@/conf.ts";
import { tokenField } from "@/conf";
import { useEffect, useReducer } from "react";
import Loader from "@/components/Loader.tsx";
import "@/assets/pages/auth.less";
@ -10,7 +10,7 @@ import router from "@/router.tsx";
import { useTranslation } from "react-i18next";
import { getQueryParam } from "@/utils/path.ts";
import { setMemory } from "@/utils/memory.ts";
import { appLogo, appName, useDeeptrain } from "@/utils/env.ts";
import { appLogo, appName, useDeeptrain } from "@/conf/env.ts";
import { Card, CardContent } from "@/components/ui/card.tsx";
import { goAuth } from "@/utils/app.ts";
import { Label } from "@/components/ui/label.tsx";

View File

@ -14,7 +14,7 @@ import Require, {
import { Input } from "@/components/ui/input.tsx";
import { Button } from "@/components/ui/button.tsx";
import TickButton from "@/components/TickButton.tsx";
import { appLogo } from "@/utils/env.ts";
import { appLogo } from "@/conf/env.ts";
function Forgot() {
const { t } = useTranslation();

View File

@ -3,7 +3,7 @@ import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { Button } from "@/components/ui/button.tsx";
import { ChevronLeft, Cloud, FileDown, Send } from "lucide-react";
import { rest_api } from "@/conf.ts";
import { apiEndpoint } from "@/conf";
import router from "@/router.tsx";
import { Input } from "@/components/ui/input.tsx";
import { useEffect, useRef, useState } from "react";
@ -12,7 +12,7 @@ import { useToast } from "@/components/ui/use-toast.ts";
import { handleGenerationData } from "@/utils/processor.ts";
import { selectModel } from "@/store/chat.ts";
import ModelFinder from "@/components/home/ModelFinder.tsx";
import { appLogo } from "@/utils/env.ts";
import { appLogo } from "@/conf/env.ts";
type WrapperProps = {
onSend?: (value: string, model: string) => boolean;
@ -111,14 +111,14 @@ function Wrapper({ onSend }: WrapperProps) {
<div className={`hash-box`}>
<a
className={`download-box`}
href={`${rest_api}/generation/download/tar?hash=${hash}`}
href={`${apiEndpoint}/generation/download/tar?hash=${hash}`}
>
<FileDown className={`h-6 w-6`} />
<p>{t("generate.download", { name: "tar.gz" })}</p>
</a>
<a
className={`download-box`}
href={`${rest_api}/generation/download/zip?hash=${hash}`}
href={`${apiEndpoint}/generation/download/zip?hash=${hash}`}
>
<FileDown className={`h-6 w-6`} />
<p>{t("generate.download", { name: "zip" })}</p>

View File

@ -16,7 +16,7 @@ import { useToast } from "@/components/ui/use-toast.ts";
import TickButton from "@/components/TickButton.tsx";
import { validateToken } from "@/store/auth.ts";
import { useDispatch } from "react-redux";
import { appLogo, appName } from "@/utils/env.ts";
import { appLogo, appName } from "@/conf/env.ts";
type CompProps = {
form: RegisterForm;

View File

@ -14,7 +14,7 @@ import {
useState,
} from "react";
import { Model as RawModel } from "@/api/types.ts";
import { supportModels } from "@/conf.ts";
import { supportModels } from "@/conf";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
import { Input } from "@/components/ui/input.tsx";
import { GripVertical, HelpCircle, Plus, Trash2 } from "lucide-react";

View File

@ -1,6 +1,6 @@
import { createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { tokenField } from "@/conf.ts";
import { tokenField } from "@/conf";
import { AppDispatch, RootState } from "./index.ts";
import { forgetMemory, setMemory } from "@/utils/memory.ts";
import { doState } from "@/api/auth.ts";

View File

@ -3,7 +3,7 @@ import { ConversationInstance, Model } from "@/api/types.ts";
import { Message } from "@/api/types.ts";
import { insertStart } from "@/utils/base.ts";
import { AppDispatch, RootState } from "./index.ts";
import { planModels, supportModels } from "@/conf.ts";
import { supportModels } from "@/conf";
import {
getArrayMemory,
getBooleanMemory,
@ -31,12 +31,6 @@ export function inModel(model: string): boolean {
);
}
export function getPlanModels(level: number): string[] {
return planModels
.filter((item) => item.level <= level)
.map((item) => item.id);
}
export function getModel(model: string | undefined | null): string {
if (supportModels.length === 0) return "";
return model && inModel(model) ? model : supportModels[0].id;

View File

@ -1,6 +1,6 @@
import router from "@/router.tsx";
import { useDeeptrain } from "@/utils/env.ts";
import { login } from "@/conf.ts";
import { useDeeptrain } from "@/conf/env.ts";
import { goDeepLogin } from "@/conf/deeptrain.ts";
export let event: BeforeInstallPromptEvent | undefined;
@ -52,5 +52,5 @@ export function navigate(path: string): void {
}
export function goAuth(): void {
useDeeptrain ? login() : navigate("/login");
useDeeptrain ? goDeepLogin() : navigate("/login");
}

View File

@ -1,3 +1,6 @@
import { useEffect, useState } from "react";
import { addEventListeners } from "@/utils/dom.ts";
export let mobile = isMobile();
window.addEventListener("resize", () => {
@ -11,3 +14,29 @@ export function isMobile(): boolean {
navigator.userAgent.includes("Mobile")
);
}
export function useMobile(): boolean {
const [mobile, setMobile] = useState<boolean>(isMobile);
useEffect(() => {
const handler = () => setMobile(isMobile);
return addEventListeners(
window,
[
"resize",
"orientationchange",
"touchstart",
"touchmove",
"touchend",
"touchcancel",
"gesturestart",
"gesturechange",
"gestureend",
],
handler,
);
}, []);
return mobile;
}

View File

@ -186,7 +186,7 @@ export function addEventListener(
}
export function addEventListeners(
el: HTMLElement,
el: Window | HTMLElement,
events: string[],
handler: EventListenerOrEventListenerObject,
): () => void {

View File

@ -50,6 +50,10 @@ func (c *Connection) GetStack() Stack {
func (c *Connection) ReadWorker() {
for {
if c.IsClosed() {
break
}
form := utils.ReadForm[conversation.FormMessage](c.conn)
if form == nil {
break
@ -61,6 +65,8 @@ func (c *Connection) ReadWorker() {
c.Write(form)
}
c.Stop()
}
func (c *Connection) Write(data *conversation.FormMessage) {
@ -70,6 +76,14 @@ func (c *Connection) Write(data *conversation.FormMessage) {
c.stack <- data
}
func (c *Connection) IsClosed() bool {
return c.conn.IsClosed()
}
func (c *Connection) Stop() {
c.Write(nil)
}
func (c *Connection) Read() *conversation.FormMessage {
form := <-c.stack
return form

View File

@ -8,13 +8,18 @@ import (
"github.com/gorilla/websocket"
"io"
"net/http"
"time"
)
type WebSocket struct {
Ctx *gin.Context
Conn *websocket.Conn
MaxTimeout time.Duration
Closed bool
}
var defaultMaxTimeout = 15 * time.Minute
func CheckUpgrader(c *gin.Context, strict bool) *websocket.Upgrader {
return &websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
@ -40,10 +45,12 @@ func NewWebsocket(c *gin.Context, strict bool) *WebSocket {
})
return nil
} else {
return &WebSocket{
instance := &WebSocket{
Ctx: c,
Conn: conn,
}
instance.Init()
return instance
}
}
@ -51,12 +58,38 @@ func NewWebsocketClient(url string) *WebSocket {
if conn, _, err := websocket.DefaultDialer.Dial(url, nil); err != nil {
return nil
} else {
return &WebSocket{
instance := &WebSocket{
Conn: conn,
}
instance.Init()
return instance
}
}
func (w *WebSocket) Init() {
w.Closed = false
w.Conn.SetCloseHandler(func(code int, text string) error {
w.Closed = true
return nil
})
w.Conn.SetPongHandler(func(appData string) error {
return w.Conn.SetReadDeadline(time.Now().Add(w.GetMaxTimeout()))
})
}
func (w *WebSocket) SetMaxTimeout(timeout time.Duration) {
w.MaxTimeout = timeout
}
func (w *WebSocket) GetMaxTimeout() time.Duration {
if w.MaxTimeout <= 0 {
return defaultMaxTimeout
}
return w.MaxTimeout
}
func (w *WebSocket) Read() (int, []byte, error) {
return w.Conn.ReadMessage()
}
@ -127,6 +160,14 @@ func (w *WebSocket) GetCache() *redis.Client {
return GetCacheFromContext(w.Ctx)
}
func (w *WebSocket) GetConn() *websocket.Conn {
return w.Conn
}
func (w *WebSocket) IsClosed() bool {
return w.Closed
}
func ReadForm[T interface{}](w *WebSocket) *T {
// golang cannot use generic type in class-like struct
// except ping