diff --git a/README.md b/README.md index 290a7f6ac..56e7f9435 100644 --- a/README.md +++ b/README.md @@ -88,10 +88,14 @@ For enterprise inquiries, please contact: **business@nextchat.dev** - [x] Share as image, share to ShareGPT [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) - [x] Desktop App with tauri - [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc. -- [ ] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) +- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) +- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) + - [x] artifacts + - [ ] network search, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) ## What's New +- 🚀 v2.14.0 Now supports Artifacts & SD - 🚀 v2.10.1 support Google Gemini Pro model. - 🚀 v2.9.11 you can use azure endpoint now. - 🚀 v2.8 now we have a client that runs across all platforms! @@ -120,15 +124,20 @@ For enterprise inquiries, please contact: **business@nextchat.dev** - [x] 分享为图片,分享到 ShareGPT 链接 [#1741](https://github.com/Yidadaa/ChatGPT-Next-Web/pull/1741) - [x] 使用 tauri 打包桌面应用 - [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm) -- [ ] 插件机制,支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) +- [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) +- [x] 插件机制,支持 artifacts,联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) + - [x] artifacts + - [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) ## 最新动态 +- 🚀 v2.14.0 现在支持 Artifacts & SD 了。 +- 🚀 v2.10.1 现在支持 Gemini Pro 模型。 +- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。 +- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。 +- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。 - 🚀 v2.0 已经发布,现在你可以使用面具功能快速创建预制对话了! 了解更多: [ChatGPT 提示词高阶技能:零次、一次和少样本提示](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/138)。 - 💡 想要更方便地随时随地使用本项目?可以试下这款桌面插件:https://github.com/mushan0x0/AI0x0.com -- 🚀 v2.7 现在可以将会话分享为图片了,也可以分享到 ShareGPT 的在线链接。 -- 🚀 v2.8 发布了横跨 Linux/Windows/MacOS 的体积极小的客户端。 -- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。 ## Get Started diff --git a/app/api/artifacts/route.ts b/app/api/artifacts/route.ts new file mode 100644 index 000000000..4707e795f --- /dev/null +++ b/app/api/artifacts/route.ts @@ -0,0 +1,73 @@ +import md5 from "spark-md5"; +import { NextRequest, NextResponse } from "next/server"; +import { getServerSideConfig } from "@/app/config/server"; + +async function handle(req: NextRequest, res: NextResponse) { + const serverConfig = getServerSideConfig(); + const storeUrl = () => + `https://api.cloudflare.com/client/v4/accounts/${serverConfig.cloudflareAccountId}/storage/kv/namespaces/${serverConfig.cloudflareKVNamespaceId}`; + const storeHeaders = () => ({ + Authorization: `Bearer ${serverConfig.cloudflareKVApiKey}`, + }); + if (req.method === "POST") { + const clonedBody = await req.text(); + const hashedCode = md5.hash(clonedBody).trim(); + const body: { + key: string; + value: string; + expiration_ttl?: number; + } = { + key: hashedCode, + value: clonedBody, + }; + try { + const ttl = parseInt(serverConfig.cloudflareKVTTL as string); + if (ttl > 60) { + body["expiration_ttl"] = ttl; + } + } catch (e) { + console.error(e); + } + const res = await fetch(`${storeUrl()}/bulk`, { + headers: { + ...storeHeaders(), + "Content-Type": "application/json", + }, + method: "PUT", + body: JSON.stringify([body]), + }); + const result = await res.json(); + console.log("save data", result); + if (result?.success) { + return NextResponse.json( + { code: 0, id: hashedCode, result }, + { status: res.status }, + ); + } + return NextResponse.json( + { error: true, msg: "Save data error" }, + { status: 400 }, + ); + } + if (req.method === "GET") { + const id = req?.nextUrl?.searchParams?.get("id"); + const res = await fetch(`${storeUrl()}/values/${id}`, { + headers: storeHeaders(), + method: "GET", + }); + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: res.headers, + }); + } + return NextResponse.json( + { error: true, msg: "Invalid request" }, + { status: 400 }, + ); +} + +export const POST = handle; +export const GET = handle; + +export const runtime = "edge"; diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index 1f55beebc..12d884635 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -25,11 +25,9 @@ export class GeminiProApi implements LLMApi { baseUrl = accessStore.googleUrl; } + const isApp = !!getClientConfig()?.isApp; if (baseUrl.length === 0) { - const isApp = !!getClientConfig()?.isApp; - baseUrl = isApp - ? DEFAULT_API_HOST + `/api/proxy/google?key=${accessStore.googleApiKey}` - : ApiPath.Google; + baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google; } if (baseUrl.endsWith("/")) { baseUrl = baseUrl.slice(0, baseUrl.length - 1); @@ -43,6 +41,10 @@ export class GeminiProApi implements LLMApi { let chatPath = [baseUrl, path].join("/"); chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse"; + // if chatPath.startsWith('http') then add key in query string + if (chatPath.startsWith("http") && accessStore.googleApiKey) { + chatPath += `&key=${accessStore.googleApiKey}`; + } return chatPath; } extractMessage(res: any) { diff --git a/app/components/artifacts.module.scss b/app/components/artifacts.module.scss new file mode 100644 index 000000000..6bd0fd9cf --- /dev/null +++ b/app/components/artifacts.module.scss @@ -0,0 +1,31 @@ +.artifacts { + display: flex; + width: 100%; + height: 100%; + flex-direction: column; + &-header { + display: flex; + align-items: center; + height: 36px; + padding: 20px; + background: var(--second); + } + &-title { + flex: 1; + text-align: center; + font-weight: bold; + font-size: 24px; + } + &-content { + flex-grow: 1; + padding: 0 20px 20px 20px; + background-color: var(--second); + } +} + +.artifacts-iframe { + width: 100%; + border: var(--border-in-light); + border-radius: 6px; + background-color: var(--gray); +} diff --git a/app/components/artifacts.tsx b/app/components/artifacts.tsx new file mode 100644 index 000000000..326891e73 --- /dev/null +++ b/app/components/artifacts.tsx @@ -0,0 +1,234 @@ +import { useEffect, useState, useRef, useMemo } from "react"; +import { useParams } from "react-router"; +import { useWindowSize } from "@/app/utils"; +import { IconButton } from "./button"; +import { nanoid } from "nanoid"; +import ExportIcon from "../icons/share.svg"; +import CopyIcon from "../icons/copy.svg"; +import DownloadIcon from "../icons/download.svg"; +import GithubIcon from "../icons/github.svg"; +import LoadingButtonIcon from "../icons/loading.svg"; +import Locale from "../locales"; +import { Modal, showToast } from "./ui-lib"; +import { copyToClipboard, downloadAs } from "../utils"; +import { Path, ApiPath, REPO_URL } from "@/app/constant"; +import { Loading } from "./home"; +import styles from "./artifacts.module.scss"; + +export function HTMLPreview(props: { + code: string; + autoHeight?: boolean; + height?: number | string; + onLoad?: (title?: string) => void; +}) { + const ref = useRef(null); + const frameId = useRef(nanoid()); + const [iframeHeight, setIframeHeight] = useState(600); + const [title, setTitle] = useState(""); + /* + * https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an + * 1. using srcdoc + * 2. using src with dataurl: + * easy to share + * length limit (Data URIs cannot be larger than 32,768 characters.) + */ + + useEffect(() => { + const handleMessage = (e: any) => { + const { id, height, title } = e.data; + setTitle(title); + if (id == frameId.current) { + setIframeHeight(height); + } + }; + window.addEventListener("message", handleMessage); + return () => { + window.removeEventListener("message", handleMessage); + }; + }, []); + + const height = useMemo(() => { + if (!props.autoHeight) return props.height || 600; + if (typeof props.height === "string") { + return props.height; + } + const parentHeight = props.height || 600; + return iframeHeight + 40 > parentHeight ? parentHeight : iframeHeight + 40; + }, [props.autoHeight, props.height, iframeHeight]); + + const srcDoc = useMemo(() => { + const script = ``; + if (props.code.includes("")) { + props.code.replace("", "" + script); + } + return props.code + script; + }, [props.code]); + + const handleOnLoad = () => { + if (props?.onLoad) { + props.onLoad(title); + } + }; + + return ( +