Skip to content

Commit 51e8f04

Browse files
committed
Merge branch 'feature-artifacts' of https://github.com/ConnectAI-E/ChatGPT-Next-Web into feature/artifacts-style
2 parents 21ef9a4 + 5ec0311 commit 51e8f04

36 files changed

Lines changed: 1756 additions & 156 deletions

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,14 @@ You can use this option if you want to increase the number of webdav service add
326326

327327
Customize the default template used to initialize the User Input Preprocessing configuration item in Settings.
328328

329+
### `STABILITY_API_KEY` (optional)
330+
331+
Stability API key.
332+
333+
### `STABILITY_URL` (optional)
334+
335+
Customize Stability API url.
336+
329337
## Requirements
330338

331339
NodeJS >= 18, Docker >= 20

README_CN.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,15 @@ ByteDance Api Url.
218218

219219
自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项
220220

221+
### `STABILITY_API_KEY` (optional)
222+
223+
Stability API密钥
224+
225+
### `STABILITY_URL` (optional)
226+
227+
自定义的Stability API请求地址
228+
229+
221230
## 开发
222231

223232
点击下方按钮,开始二次开发:

app/api/artifact/route.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,37 @@ import { getServerSideConfig } from "@/app/config/server";
44

55
async function handle(req: NextRequest, res: NextResponse) {
66
const serverConfig = getServerSideConfig();
7-
const storeUrl = (key: string) =>
8-
`https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}/values/${key}`;
7+
const storeUrl = () =>
8+
`https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`;
99
const storeHeaders = () => ({
1010
Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`,
1111
});
1212
if (req.method === "POST") {
1313
const clonedBody = await req.text();
1414
const hashedCode = md5.hash(clonedBody).trim();
15-
const res = await fetch(storeUrl(hashedCode), {
16-
headers: storeHeaders(),
15+
const body: {
16+
key: string;
17+
value: string;
18+
expiration_ttl?: number;
19+
} = {
20+
key: hashedCode,
21+
value: clonedBody,
22+
};
23+
try {
24+
const ttl = parseInt(serverConfig.cloudflareKVTTL as string);
25+
if (ttl > 60) {
26+
body["expiration_ttl"] = ttl;
27+
}
28+
} catch (e) {
29+
console.error(e);
30+
}
31+
const res = await fetch(`${storeUrl()}/bulk`, {
32+
headers: {
33+
...storeHeaders(),
34+
"Content-Type": "application/json",
35+
},
1736
method: "PUT",
18-
body: clonedBody,
37+
body: JSON.stringify([body]),
1938
});
2039
const result = await res.json();
2140
console.log("save data", result);
@@ -32,7 +51,7 @@ async function handle(req: NextRequest, res: NextResponse) {
3251
}
3352
if (req.method === "GET") {
3453
const id = req?.nextUrl?.searchParams?.get("id");
35-
const res = await fetch(storeUrl(id as string), {
54+
const res = await fetch(`${storeUrl()}/values/${id}`, {
3655
headers: storeHeaders(),
3756
method: "GET",
3857
});

app/api/auth.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
6767
let systemApiKey: string | undefined;
6868

6969
switch (modelProvider) {
70+
case ModelProvider.Stability:
71+
systemApiKey = serverConfig.stabilityApiKey;
72+
break;
7073
case ModelProvider.GeminiPro:
7174
systemApiKey = serverConfig.googleApiKey;
7275
break;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getServerSideConfig } from "@/app/config/server";
3+
import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant";
4+
import { auth } from "@/app/api/auth";
5+
6+
async function handle(
7+
req: NextRequest,
8+
{ params }: { params: { path: string[] } },
9+
) {
10+
console.log("[Stability] params ", params);
11+
12+
if (req.method === "OPTIONS") {
13+
return NextResponse.json({ body: "OK" }, { status: 200 });
14+
}
15+
16+
const controller = new AbortController();
17+
18+
const serverConfig = getServerSideConfig();
19+
20+
let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL;
21+
22+
if (!baseUrl.startsWith("http")) {
23+
baseUrl = `https://${baseUrl}`;
24+
}
25+
26+
if (baseUrl.endsWith("/")) {
27+
baseUrl = baseUrl.slice(0, -1);
28+
}
29+
30+
let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", "");
31+
32+
console.log("[Stability Proxy] ", path);
33+
console.log("[Stability Base Url]", baseUrl);
34+
35+
const timeoutId = setTimeout(
36+
() => {
37+
controller.abort();
38+
},
39+
10 * 60 * 1000,
40+
);
41+
42+
const authResult = auth(req, ModelProvider.Stability);
43+
44+
if (authResult.error) {
45+
return NextResponse.json(authResult, {
46+
status: 401,
47+
});
48+
}
49+
50+
const bearToken = req.headers.get("Authorization") ?? "";
51+
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
52+
53+
const key = token ? token : serverConfig.stabilityApiKey;
54+
55+
if (!key) {
56+
return NextResponse.json(
57+
{
58+
error: true,
59+
message: `missing STABILITY_API_KEY in server env vars`,
60+
},
61+
{
62+
status: 401,
63+
},
64+
);
65+
}
66+
67+
const fetchUrl = `${baseUrl}/${path}`;
68+
console.log("[Stability Url] ", fetchUrl);
69+
const fetchOptions: RequestInit = {
70+
headers: {
71+
"Content-Type": req.headers.get("Content-Type") || "multipart/form-data",
72+
Accept: req.headers.get("Accept") || "application/json",
73+
Authorization: `Bearer ${key}`,
74+
},
75+
method: req.method,
76+
body: req.body,
77+
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
78+
redirect: "manual",
79+
// @ts-ignore
80+
duplex: "half",
81+
signal: controller.signal,
82+
};
83+
84+
try {
85+
const res = await fetch(fetchUrl, fetchOptions);
86+
// to prevent browser prompt for credentials
87+
const newHeaders = new Headers(res.headers);
88+
newHeaders.delete("www-authenticate");
89+
// to disable nginx buffering
90+
newHeaders.set("X-Accel-Buffering", "no");
91+
return new Response(res.body, {
92+
status: res.status,
93+
statusText: res.statusText,
94+
headers: newHeaders,
95+
});
96+
} finally {
97+
clearTimeout(timeoutId);
98+
}
99+
}
100+
101+
export const GET = handle;
102+
export const POST = handle;
103+
104+
export const runtime = "edge";

app/api/webdav/[...path]/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,13 @@ async function handle(
3737
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
3838
const normalizedEndpoint = normalizeUrl(endpoint as string);
3939

40-
return normalizedEndpoint &&
40+
return (
41+
normalizedEndpoint &&
4142
normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname &&
42-
normalizedEndpoint.pathname.startsWith(normalizedAllowedEndpoint.pathname);
43+
normalizedEndpoint.pathname.startsWith(
44+
normalizedAllowedEndpoint.pathname,
45+
)
46+
);
4347
})
4448
) {
4549
return NextResponse.json(

app/client/api.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,19 @@ export class ClientApi {
168168
}
169169
}
170170

171+
export function getBearerToken(
172+
apiKey: string,
173+
noBearer: boolean = false,
174+
): string {
175+
return validString(apiKey)
176+
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
177+
: "";
178+
}
179+
180+
export function validString(x: string): boolean {
181+
return x?.length > 0;
182+
}
183+
171184
export function getHeaders() {
172185
const accessStore = useAccessStore.getState();
173186
const chatStore = useChatStore.getState();
@@ -214,15 +227,6 @@ export function getHeaders() {
214227
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
215228
}
216229

217-
function getBearerToken(apiKey: string, noBearer: boolean = false): string {
218-
return validString(apiKey)
219-
? `${noBearer ? "" : "Bearer "}${apiKey.trim()}`
220-
: "";
221-
}
222-
223-
function validString(x: string): boolean {
224-
return x?.length > 0;
225-
}
226230
const {
227231
isGoogle,
228232
isAzure,

app/components/artifact.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,18 @@ export function HTMLPreview(props: {
3434
*/
3535

3636
useEffect(() => {
37-
window.addEventListener("message", (e) => {
37+
const handleMessage = (e: any) => {
3838
const { id, height, title } = e.data;
3939
setTitle(title);
4040
if (id == frameId.current) {
4141
setIframeHeight(height);
4242
}
43-
});
44-
}, [iframeHeight]);
43+
};
44+
window.addEventListener("message", handleMessage);
45+
return () => {
46+
window.removeEventListener("message", handleMessage);
47+
};
48+
}, []);
4549

4650
const height = useMemo(() => {
4751
const parentHeight = props.height || 600;
@@ -186,8 +190,17 @@ export function Artifact() {
186190
useEffect(() => {
187191
if (id) {
188192
fetch(`${ApiPath.Artifact}?id=${id}`)
193+
.then((res) => {
194+
if (res.status > 300) {
195+
throw Error("can not get content");
196+
}
197+
return res;
198+
})
189199
.then((res) => res.text())
190-
.then(setCode);
200+
.then(setCode)
201+
.catch((e) => {
202+
showToast(Locale.Export.Artifact.Error);
203+
});
191204
}
192205
}, [id]);
193206

app/components/button.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from "react";
22

33
import styles from "./button.module.scss";
4+
import { CSSProperties } from "react";
45

56
export type ButtonType = "primary" | "danger" | null;
67

@@ -16,6 +17,7 @@ export function IconButton(props: {
1617
disabled?: boolean;
1718
tabIndex?: number;
1819
autoFocus?: boolean;
20+
style?: CSSProperties;
1921
}) {
2022
return (
2123
<button
@@ -31,6 +33,7 @@ export function IconButton(props: {
3133
role="button"
3234
tabIndex={props.tabIndex}
3335
autoFocus={props.autoFocus}
36+
style={props.style}
3437
>
3538
{props.icon && (
3639
<div

app/components/chat.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ function ClearContextDivider() {
340340
);
341341
}
342342

343-
function ChatAction(props: {
343+
export function ChatAction(props: {
344344
text: string;
345345
icon: JSX.Element;
346346
onClick: () => void;

0 commit comments

Comments
 (0)