Add ShareBtn component and update SettingsOptionLayout
This commit is contained in:
parent
3eabe10bde
commit
7ce79bb134
2
page-share.md
Normal file
2
page-share.md
Normal file
@ -0,0 +1,2 @@
|
||||
# Page Share
|
||||
|
193
src/components/Common/ShareBtn.tsx
Normal file
193
src/components/Common/ShareBtn.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import { Form, Image, Input, Modal, Tooltip, message } from "antd"
|
||||
import { Share } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import type { Message } from "~store/option"
|
||||
import Markdown from "./Markdown"
|
||||
import React from "react"
|
||||
import { useMutation } from "@tanstack/react-query"
|
||||
import { getPageShareUrl } from "~services/ollama"
|
||||
import { cleanUrl } from "~libs/clean-url"
|
||||
import { getUserId, saveWebshare } from "~libs/db"
|
||||
|
||||
type Props = {
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
const reformatMessages = (messages: Message[], username: string) => {
|
||||
return messages.map((message, idx) => {
|
||||
return {
|
||||
id: idx,
|
||||
name: message.isBot ? message.name : username,
|
||||
isBot: message.isBot,
|
||||
message: message.message,
|
||||
images: message.images
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const PlaygroundMessage = (
|
||||
props: Message & {
|
||||
username: string
|
||||
}
|
||||
) => {
|
||||
return (
|
||||
<div className="group w-full text-gray-800 dark:text-gray-100">
|
||||
<div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl flex lg:px-0 m-auto w-full">
|
||||
<div className="flex flex-row gap-4 md:gap-6 md:max-w-2xl lg:max-w-xl xl:max-w-3xl m-auto w-full">
|
||||
<div className="w-8 flex flex-col relative items-end">
|
||||
<div className="relative h-7 w-7 p-1 rounded-sm text-white flex items-center justify-center text-opacity-100r">
|
||||
{props.isBot ? (
|
||||
<div className="absolute h-8 w-8 rounded-full bg-gradient-to-r from-green-300 to-purple-400"></div>
|
||||
) : (
|
||||
<div className="absolute h-8 w-8 rounded-full from-blue-400 to-blue-600 bg-gradient-to-r"></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-[calc(100%-50px)] flex-col gap-3 lg:w-[calc(100%-115px)]">
|
||||
<span className="text-xs font-bold text-gray-800 dark:text-white">
|
||||
{props.isBot ? props.name : props.username}
|
||||
</span>
|
||||
|
||||
<div className="flex flex-grow flex-col">
|
||||
<Markdown message={props.message} />
|
||||
</div>
|
||||
{/* source if aviable */}
|
||||
{props.images && props.images.length > 0 && (
|
||||
<div className="flex md:max-w-2xl lg:max-w-xl xl:max-w-3xl mt-4 m-auto w-full">
|
||||
{props.images
|
||||
.filter((image) => image.length > 0)
|
||||
.map((image, index) => (
|
||||
<Image
|
||||
key={index}
|
||||
src={image}
|
||||
alt="Uploaded Image"
|
||||
width={180}
|
||||
className="rounded-md relative"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ShareBtn: React.FC<Props> = ({ messages }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
const name = Form.useWatch("name", form)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
form.setFieldsValue({
|
||||
title: messages[0].message
|
||||
})
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
const onSubmit = async (values: { title: string; name: string }) => {
|
||||
const owner_id = await getUserId()
|
||||
const chat = reformatMessages(messages, values.name)
|
||||
const title = values.title
|
||||
const url = await getPageShareUrl()
|
||||
const res = await fetch(`${cleanUrl(url)}/api/v1/share/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
owner_id,
|
||||
messages: chat,
|
||||
title
|
||||
})
|
||||
})
|
||||
|
||||
if (!res.ok) throw new Error("Failed to create share link")
|
||||
|
||||
const data = await res.json()
|
||||
|
||||
return {
|
||||
...data,
|
||||
url: `${cleanUrl(url)}/share/${data.chat_id}`,
|
||||
api_url: cleanUrl(url),
|
||||
share_id: data.chat_id
|
||||
}
|
||||
}
|
||||
|
||||
const { mutate: createShareLink, isPending } = useMutation({
|
||||
mutationFn: onSubmit,
|
||||
onSuccess: async (data) => {
|
||||
const url = data.url
|
||||
navigator.clipboard.writeText(url)
|
||||
message.success("Link copied to clipboard")
|
||||
await saveWebshare({ title: data.title, url, api_url: data.api_url, share_id: data.share_id })
|
||||
setOpen(false)
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(error?.message || "Failed to create share link")
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Share">
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="!text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
|
||||
<Share className="w-6 h-6" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Modal
|
||||
title="Share link to Chat"
|
||||
open={open}
|
||||
footer={null}
|
||||
width={600}
|
||||
onCancel={() => setOpen(false)}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={createShareLink}
|
||||
initialValues={{
|
||||
title: "Untitled Chat",
|
||||
name: "Anonymous"
|
||||
}}>
|
||||
<Form.Item
|
||||
name="title"
|
||||
label="Chat Title"
|
||||
rules={[{ required: true, message: "Please enter chat title" }]}>
|
||||
<Input size="large" placeholder="Enter chat title" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="Your Name"
|
||||
rules={[{ required: true, message: "Please enter your name" }]}>
|
||||
<Input size="large" placeholder="Enter your name" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div className="max-h-[180px] overflow-x-auto border dark:border-gray-700 rounded-md p-2">
|
||||
<div className="flex flex-col p-3">
|
||||
{messages.map((message, index) => (
|
||||
<PlaygroundMessage key={index} {...message} username={name} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex items-center rounded-md border border-transparent bg-black px-2 py-2.5 text-md font-medium leading-4 text-white shadow-sm dark:bg-white dark:text-gray-800 disabled:opacity-50 ">
|
||||
{isPending ? "Generating link..." : "Generate Link"}
|
||||
</button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
@ -16,6 +16,7 @@ import {
|
||||
ZapIcon
|
||||
} from "lucide-react"
|
||||
import { getAllPrompts } from "~libs/db"
|
||||
import { ShareBtn } from "~components/Common/ShareBtn"
|
||||
|
||||
export default function OptionLayout({
|
||||
children
|
||||
@ -29,7 +30,9 @@ export default function OptionLayout({
|
||||
clearChat,
|
||||
selectedSystemPrompt,
|
||||
setSelectedQuickPrompt,
|
||||
setSelectedSystemPrompt
|
||||
setSelectedSystemPrompt,
|
||||
messages,
|
||||
streaming
|
||||
} = useMessageOption()
|
||||
|
||||
const {
|
||||
@ -155,6 +158,9 @@ export default function OptionLayout({
|
||||
<div className="flex flex-1 justify-end px-4">
|
||||
<div className="ml-4 flex items-center md:ml-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
{pathname === "/" && messages.length > 0 && !streaming && (
|
||||
<ShareBtn messages={messages} />
|
||||
)}
|
||||
{/* <Tooltip title="Manage Prompts">
|
||||
<NavLink
|
||||
to="/prompts"
|
||||
|
@ -2,7 +2,8 @@ import {
|
||||
Book,
|
||||
BrainCircuit,
|
||||
CircuitBoardIcon,
|
||||
Orbit
|
||||
Orbit,
|
||||
Share
|
||||
} from "lucide-react"
|
||||
import { Link, useLocation } from "react-router-dom"
|
||||
|
||||
@ -46,7 +47,7 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<div className="mx-auto max-w-7xl lg:flex lg:gap-x-16 lg:px-8">
|
||||
<aside className="flex lg:rounded-md bg-white lg:h-56 lg:p-4 lg:mt-20 overflow-x-auto lg:border border-b py-4 lg:block lg:w-64 lg:flex-none dark:bg-[#171717] dark:border-gray-600">
|
||||
<aside className="flex lg:rounded-md bg-white lg:p-4 lg:mt-20 overflow-x-auto lg:border-0 border-b py-4 lg:block lg:w-64 lg:flex-none dark:bg-[#171717] dark:border-gray-600">
|
||||
<nav className="flex-none px-4 sm:px-6 lg:px-0">
|
||||
<ul
|
||||
role="list"
|
||||
@ -75,6 +76,12 @@ export const SettingsLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
icon={Book}
|
||||
current={location.pathname}
|
||||
/>
|
||||
<LinkComponent
|
||||
href="/settings/share"
|
||||
name="Manage Share"
|
||||
icon={Share}
|
||||
current={location.pathname}
|
||||
/>
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
|
190
src/components/Option/Share/index.tsx
Normal file
190
src/components/Option/Share/index.tsx
Normal file
@ -0,0 +1,190 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { Form, Input, Skeleton, Table, Tooltip, message } from "antd"
|
||||
import { Trash2 } from "lucide-react"
|
||||
import { SaveButton } from "~components/Common/SaveButton"
|
||||
import { deleteWebshare, getAllWebshares, getUserId } from "~libs/db"
|
||||
import { getPageShareUrl, setPageShareUrl } from "~services/ollama"
|
||||
import { verifyPageShareURL } from "~utils/verify-page-share"
|
||||
|
||||
export const OptionShareBody = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const { status, data } = useQuery({
|
||||
queryKey: ["fetchShareInfo"],
|
||||
queryFn: async () => {
|
||||
const [url, shares] = await Promise.all([
|
||||
getPageShareUrl(),
|
||||
getAllWebshares()
|
||||
])
|
||||
return { url, shares }
|
||||
}
|
||||
})
|
||||
|
||||
const onSubmit = async (values: { url: string }) => {
|
||||
const isOk = await verifyPageShareURL(values.url)
|
||||
if (isOk) {
|
||||
await setPageShareUrl(values.url)
|
||||
}
|
||||
}
|
||||
|
||||
const onDelete = async ({
|
||||
api_url,
|
||||
share_id,
|
||||
id
|
||||
}: {
|
||||
id: string
|
||||
share_id: string
|
||||
api_url: string
|
||||
}) => {
|
||||
const owner_id = await getUserId()
|
||||
const res = await fetch(`${api_url}/api/v1/share/delete`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
share_id,
|
||||
owner_id
|
||||
})
|
||||
})
|
||||
if (!res.ok) throw new Error("Failed to delete share link")
|
||||
await deleteWebshare(id)
|
||||
return "ok"
|
||||
}
|
||||
|
||||
const { mutate: updatePageShareUrl, isPending: isUpdatePending } =
|
||||
useMutation({
|
||||
mutationFn: onSubmit,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["fetchShareInfo"]
|
||||
})
|
||||
message.success("Page Share URL updated successfully")
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(error?.message || "Failed to update Page Share URL")
|
||||
}
|
||||
})
|
||||
|
||||
const { mutate: deleteMutation } = useMutation({
|
||||
mutationFn: onDelete,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["fetchShareInfo"]
|
||||
})
|
||||
message.success("Webshare deleted successfully")
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(error?.message || "Failed to delete Webshare")
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-3">
|
||||
{status === "pending" && <Skeleton paragraph={{ rows: 4 }} active />}
|
||||
{status === "success" && (
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
|
||||
Configure Page Share URL
|
||||
</h2>
|
||||
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
|
||||
</div>
|
||||
<Form
|
||||
layout="vertical"
|
||||
onFinish={updatePageShareUrl}
|
||||
initialValues={{
|
||||
url: data.url
|
||||
}}>
|
||||
<Form.Item
|
||||
name="url"
|
||||
help={
|
||||
<span>
|
||||
For privacy reasons, you can self-host the page share and
|
||||
provide the URL here.{" "}
|
||||
<a
|
||||
href="https://github.com/n4ze3m/page-assist/blob/main/page-share.md"
|
||||
target="__blank"
|
||||
className="text-blue-600 dark:text-blue-400">
|
||||
Learn more
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please input your Page Share URL!"
|
||||
}
|
||||
]}
|
||||
label="Page Share URL">
|
||||
<Input placeholder="Page Share URL" size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<div className="flex justify-end">
|
||||
<SaveButton disabled={isUpdatePending} btnType="submit" />
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold leading-7 text-gray-900 dark:text-white">
|
||||
Webshares
|
||||
</h2>
|
||||
<div className="border border-b border-gray-200 dark:border-gray-600 mt-3 mb-6"></div>
|
||||
</div>
|
||||
<div>
|
||||
<Table
|
||||
dataSource={data.shares}
|
||||
columns={[
|
||||
{
|
||||
title: "Title",
|
||||
dataIndex: "title",
|
||||
key: "title"
|
||||
},
|
||||
{
|
||||
title: "URL",
|
||||
dataIndex: "url",
|
||||
key: "url",
|
||||
render: (url: string) => (
|
||||
<a
|
||||
href={url}
|
||||
target="__blank"
|
||||
className="text-blue-600 dark:text-blue-400">
|
||||
{url}
|
||||
</a>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
render: (_, render) => (
|
||||
<Tooltip title="Delete Share">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to delete this webshare?"
|
||||
)
|
||||
) {
|
||||
deleteMutation({
|
||||
id: render.id,
|
||||
share_id: render.share_id,
|
||||
api_url: render.api_url
|
||||
})
|
||||
}
|
||||
}}
|
||||
className="text-red-500 dark:text-red-400">
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -32,6 +32,16 @@ type Message = {
|
||||
}
|
||||
|
||||
|
||||
type Webshare = {
|
||||
id: string
|
||||
title: string
|
||||
url: string
|
||||
api_url: string
|
||||
share_id: string
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
|
||||
type Prompt = {
|
||||
id: string
|
||||
title: string
|
||||
@ -154,6 +164,47 @@ export class PageAssitDatabase {
|
||||
return prompts.find((prompt) => prompt.id === id)
|
||||
}
|
||||
|
||||
|
||||
async getWebshare(id: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get(id, (result) => {
|
||||
resolve(result[id] || [])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async getAllWebshares(): Promise<Webshare[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get("webshares", (result) => {
|
||||
resolve(result.webshares || [])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async addWebshare(webshare: Webshare) {
|
||||
const webshares = await this.getAllWebshares()
|
||||
const newWebshares = [webshare, ...webshares]
|
||||
this.db.set({ webshares: newWebshares })
|
||||
}
|
||||
|
||||
async deleteWebshare(id: string) {
|
||||
const webshares = await this.getAllWebshares()
|
||||
const newWebshares = webshares.filter((webshare) => webshare.id !== id)
|
||||
this.db.set({ webshares: newWebshares })
|
||||
}
|
||||
|
||||
async getUserID() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db.get("user_id", (result) => {
|
||||
resolve(result.user_id || "")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async setUserID(id: string) {
|
||||
this.db.set({ user_id: id })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -277,4 +328,39 @@ export const getPromptById = async (id: string) => {
|
||||
if (!id || id.trim() === "") return null
|
||||
const db = new PageAssitDatabase()
|
||||
return await db.getPromptById(id)
|
||||
}
|
||||
|
||||
export const getAllWebshares = async () => {
|
||||
const db = new PageAssitDatabase()
|
||||
return await db.getAllWebshares()
|
||||
}
|
||||
|
||||
export const deleteWebshare = async (id: string) => {
|
||||
const db = new PageAssitDatabase()
|
||||
await db.deleteWebshare(id)
|
||||
return id
|
||||
|
||||
}
|
||||
|
||||
export const saveWebshare = async ({ title, url, api_url, share_id }: { title: string, url: string, api_url: string, share_id: string }) => {
|
||||
const db = new PageAssitDatabase()
|
||||
const id = generateID()
|
||||
const createdAt = Date.now()
|
||||
const webshare = { id, title, url, share_id, createdAt, api_url }
|
||||
await db.addWebshare(webshare)
|
||||
return webshare
|
||||
}
|
||||
|
||||
export const getUserId = async () => {
|
||||
const db = new PageAssitDatabase()
|
||||
const id = await db.getUserID() as string
|
||||
if (!id || id?.trim() === "") {
|
||||
const user_id = "user_xxxx-xxxx-xxx-xxxx-xxxx".replace(/[x]/g, () => {
|
||||
const r = Math.floor(Math.random() * 16)
|
||||
return r.toString(16)
|
||||
})
|
||||
db.setUserID(user_id)
|
||||
return user_id
|
||||
}
|
||||
return id
|
||||
}
|
@ -7,6 +7,7 @@ import { OptionModal } from "./option-settings-model"
|
||||
import { OptionPrompt } from "./option-settings-prompt"
|
||||
import { OptionOllamaSettings } from "./options-settings-ollama"
|
||||
import { OptionSettings } from "./option-settings"
|
||||
import { OptionShare } from "./option-settings-share"
|
||||
|
||||
export const OptionRouting = () => {
|
||||
const { mode } = useDarkMode()
|
||||
@ -19,6 +20,7 @@ export const OptionRouting = () => {
|
||||
<Route path="/settings/model" element={<OptionModal />} />
|
||||
<Route path="/settings/prompt" element={<OptionPrompt />} />
|
||||
<Route path="/settings/ollama" element={<OptionOllamaSettings />} />
|
||||
<Route path="/settings/share" element={<OptionShare />} />
|
||||
</Routes>
|
||||
</div>
|
||||
)
|
||||
|
13
src/routes/option-settings-share.tsx
Normal file
13
src/routes/option-settings-share.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { SettingsLayout } from "~components/Layouts/SettingsOptionLayout"
|
||||
import OptionLayout from "~components/Layouts/Layout"
|
||||
import { OptionShareBody } from "~components/Option/Share"
|
||||
|
||||
export const OptionShare = () => {
|
||||
return (
|
||||
<OptionLayout>
|
||||
<SettingsLayout>
|
||||
<OptionShareBody />
|
||||
</SettingsLayout>
|
||||
</OptionLayout>
|
||||
)
|
||||
}
|
@ -6,6 +6,7 @@ const storage = new Storage()
|
||||
|
||||
const DEFAULT_OLLAMA_URL = "http://127.0.0.1:11434"
|
||||
const DEFAULT_ASK_FOR_MODEL_SELECTION_EVERY_TIME = true
|
||||
const DEFAULT_PAGE_SHARE_URL = "https://pageassist.xyz"
|
||||
|
||||
const DEFAULT_RAG_QUESTION_PROMPT =
|
||||
"Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question. Chat History: {chat_history} Follow Up Input: {question} Standalone question:"
|
||||
@ -302,4 +303,17 @@ export const getIsSimpleInternetSearch = async () => {
|
||||
|
||||
export const setIsSimpleInternetSearch = async (isSimpleInternetSearch: boolean) => {
|
||||
await storage.set("isSimpleInternetSearch", isSimpleInternetSearch.toString())
|
||||
}
|
||||
|
||||
export const getPageShareUrl = async () => {
|
||||
const pageShareUrl = await storage.get("pageShareUrl")
|
||||
if (!pageShareUrl || pageShareUrl.length === 0) {
|
||||
return DEFAULT_PAGE_SHARE_URL
|
||||
}
|
||||
return pageShareUrl
|
||||
}
|
||||
|
||||
|
||||
export const setPageShareUrl = async (pageShareUrl: string) => {
|
||||
await storage.set("pageShareUrl", pageShareUrl)
|
||||
}
|
10
src/utils/verify-page-share.ts
Normal file
10
src/utils/verify-page-share.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { cleanUrl } from "~libs/clean-url"
|
||||
|
||||
export const verifyPageShareURL = async (url: string) => {
|
||||
const res = await fetch(`${cleanUrl(url)}/api/v1/ping`)
|
||||
if (!res.ok) {
|
||||
throw new Error("Unable to verify page share")
|
||||
}
|
||||
const data = await res.text()
|
||||
return data === "pong"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user