eb04c344a5
- Updated message types in support-host-bridge.ts to use agent-desk prefix. - Added new agent-desk-sdk.min.js file with updated functionality. - Removed old cs-ai-agent-sdk.min.js file. - Modified build-sdk script to generate agent-desk-sdk.min.js. - Changed scrollbar class names in main.scss from cs-ai-agent to agent-desk.
233 lines
7.3 KiB
TypeScript
233 lines
7.3 KiB
TypeScript
import type { Editor } from "@tiptap/react"
|
|
import Image from "@tiptap/extension-image"
|
|
|
|
export const MessageImageExtension = Image.extend({
|
|
addAttributes() {
|
|
return {
|
|
...this.parent?.(),
|
|
dataAssetId: {
|
|
default: null,
|
|
parseHTML: (element) => element.getAttribute("data-asset-id"),
|
|
renderHTML: (attributes) =>
|
|
attributes.dataAssetId ? { "data-asset-id": attributes.dataAssetId } : {},
|
|
},
|
|
dataProvider: {
|
|
default: null,
|
|
parseHTML: (element) => element.getAttribute("data-provider"),
|
|
renderHTML: (attributes) =>
|
|
attributes.dataProvider ? { "data-provider": attributes.dataProvider } : {},
|
|
},
|
|
dataStorageKey: {
|
|
default: null,
|
|
parseHTML: (element) => element.getAttribute("data-storage-key"),
|
|
renderHTML: (attributes) =>
|
|
attributes.dataStorageKey ? { "data-storage-key": attributes.dataStorageKey } : {},
|
|
},
|
|
}
|
|
},
|
|
|
|
addNodeView() {
|
|
return ({ node }) => {
|
|
const wrapper = document.createElement("span")
|
|
wrapper.className =
|
|
"agent-desk-editor-image-wrap relative inline-block max-w-full align-middle"
|
|
|
|
const image = document.createElement("img")
|
|
image.className = "agent-desk-editor-image"
|
|
image.draggable = true
|
|
|
|
const overlay = document.createElement("span")
|
|
overlay.className =
|
|
"agent-desk-editor-image-loading pointer-events-none absolute inset-0 hidden items-center justify-center rounded-lg bg-background/55 backdrop-blur-[1px]"
|
|
|
|
const spinner = document.createElement("span")
|
|
spinner.className =
|
|
"size-6 animate-spin rounded-full border-2 border-primary/25 border-t-primary shadow-sm"
|
|
|
|
overlay.appendChild(spinner)
|
|
wrapper.appendChild(image)
|
|
wrapper.appendChild(overlay)
|
|
|
|
const applyAttrs = (attrs: Record<string, unknown>) => {
|
|
setImageAttr(image, "src", attrs.src)
|
|
setImageAttr(image, "alt", attrs.alt)
|
|
setImageAttr(image, "title", attrs.title)
|
|
setImageAttr(image, "data-asset-id", attrs.dataAssetId)
|
|
setImageAttr(image, "data-provider", attrs.dataProvider)
|
|
setImageAttr(image, "data-storage-key", attrs.dataStorageKey)
|
|
setImageUploading(wrapper, Boolean(String(attrs.title ?? "").startsWith("uploading-")))
|
|
}
|
|
|
|
applyAttrs(node.attrs)
|
|
|
|
return {
|
|
dom: wrapper,
|
|
update: (updatedNode) => {
|
|
if (updatedNode.type.name !== node.type.name) {
|
|
return false
|
|
}
|
|
applyAttrs(updatedNode.attrs)
|
|
return true
|
|
},
|
|
ignoreMutation: () => true,
|
|
}
|
|
}
|
|
},
|
|
})
|
|
|
|
export type UploadedEditorImage = {
|
|
assetId: string
|
|
provider: string
|
|
storageKey: string
|
|
filename?: string
|
|
}
|
|
|
|
export type UploadedEditorImageMap = Map<string, UploadedEditorImage>
|
|
|
|
export function removeEditorImageByTitle(editor: Editor, title: string) {
|
|
const { state } = editor
|
|
let targetPos: number | null = null
|
|
state.doc.descendants((node, pos) => {
|
|
if (node.type.name === "image" && node.attrs.title === title) {
|
|
targetPos = pos
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
if (targetPos === null) {
|
|
return
|
|
}
|
|
editor.chain().focus().deleteRange({ from: targetPos, to: targetPos + 1 }).run()
|
|
}
|
|
|
|
export function markEditorImageUploadedByTitle(
|
|
editor: Editor,
|
|
title: string,
|
|
uploaded: UploadedEditorImage,
|
|
uploadedImages: UploadedEditorImageMap
|
|
) {
|
|
uploadedImages.set(title, uploaded)
|
|
const image = findEditorImageElementByTitle(editor, title)
|
|
if (!image) {
|
|
return
|
|
}
|
|
image.setAttribute("data-asset-id", uploaded.assetId)
|
|
image.setAttribute("data-provider", uploaded.provider)
|
|
image.setAttribute("data-storage-key", uploaded.storageKey)
|
|
image.setAttribute("alt", uploaded.filename || image.getAttribute("alt") || "image")
|
|
image.classList.remove("agent-desk-editor-image-uploading")
|
|
image.removeAttribute("data-uploading")
|
|
image.removeAttribute("title")
|
|
setImageUploading(image.closest(".agent-desk-editor-image-wrap"), false)
|
|
}
|
|
|
|
export function setEditorImageUploadingByTitle(editor: Editor, title: string) {
|
|
const image = findEditorImageElementByTitle(editor, title)
|
|
if (!image) {
|
|
return
|
|
}
|
|
image.classList.add("agent-desk-editor-image-uploading")
|
|
image.setAttribute("data-uploading", "true")
|
|
setImageUploading(image.closest(".agent-desk-editor-image-wrap"), true)
|
|
}
|
|
|
|
export function buildSendableEditorHTML(
|
|
html: string,
|
|
uploadedImages?: UploadedEditorImageMap
|
|
) {
|
|
if (typeof document === "undefined" || !html.includes("<img")) {
|
|
return html
|
|
}
|
|
|
|
const template = document.createElement("template")
|
|
template.innerHTML = html
|
|
for (const image of Array.from(template.content.querySelectorAll("img"))) {
|
|
const title = image.getAttribute("title") ?? ""
|
|
const uploaded = title ? uploadedImages?.get(title) : undefined
|
|
if (uploaded) {
|
|
image.setAttribute("data-asset-id", uploaded.assetId)
|
|
image.setAttribute("data-provider", uploaded.provider)
|
|
image.setAttribute("data-storage-key", uploaded.storageKey)
|
|
image.setAttribute("alt", uploaded.filename || image.getAttribute("alt") || "image")
|
|
image.removeAttribute("title")
|
|
}
|
|
if (
|
|
image.getAttribute("data-asset-id") &&
|
|
image.getAttribute("src")?.startsWith("blob:")
|
|
) {
|
|
image.removeAttribute("src")
|
|
}
|
|
}
|
|
return template.innerHTML
|
|
}
|
|
|
|
export function hasUploadingEditorImages(
|
|
html: string,
|
|
uploadedImages?: UploadedEditorImageMap
|
|
) {
|
|
if (typeof document === "undefined" || !html.includes("<img")) {
|
|
return /<img\b[^>]*\btitle=(["'])uploading-[^"']+\1/i.test(html)
|
|
}
|
|
|
|
const template = document.createElement("template")
|
|
template.innerHTML = html
|
|
return Array.from(template.content.querySelectorAll("img")).some((image) =>
|
|
isUnfinishedUploadingImage(image, uploadedImages)
|
|
)
|
|
}
|
|
|
|
export function revokeEditorObjectUrl(urls: Set<string>, objectUrl: string) {
|
|
if (!urls.delete(objectUrl)) {
|
|
return
|
|
}
|
|
URL.revokeObjectURL(objectUrl)
|
|
}
|
|
|
|
export function revokeEditorObjectUrls(urls: Set<string>) {
|
|
for (const objectUrl of urls) {
|
|
URL.revokeObjectURL(objectUrl)
|
|
}
|
|
urls.clear()
|
|
}
|
|
|
|
function findEditorImageElementByTitle(editor: Editor, title: string) {
|
|
const escapedTitle =
|
|
typeof CSS !== "undefined" && typeof CSS.escape === "function"
|
|
? CSS.escape(title)
|
|
: title.replace(/["\\]/g, "\\$&")
|
|
return editor.view.dom.querySelector<HTMLImageElement>(
|
|
`img[title="${escapedTitle}"]`
|
|
)
|
|
}
|
|
|
|
function isUnfinishedUploadingImage(
|
|
image: HTMLImageElement,
|
|
uploadedImages?: UploadedEditorImageMap
|
|
) {
|
|
const title = image.getAttribute("title") ?? ""
|
|
return title.startsWith("uploading-") && !uploadedImages?.has(title)
|
|
}
|
|
|
|
function setImageAttr(image: HTMLImageElement, name: string, value: unknown) {
|
|
const normalized = typeof value === "string" ? value : ""
|
|
if (!normalized) {
|
|
image.removeAttribute(name)
|
|
return
|
|
}
|
|
if (image.getAttribute(name) !== normalized) {
|
|
image.setAttribute(name, normalized)
|
|
}
|
|
}
|
|
|
|
function setImageUploading(wrapper: Element | null, uploading: boolean) {
|
|
if (!(wrapper instanceof HTMLElement)) {
|
|
return
|
|
}
|
|
wrapper.classList.toggle("agent-desk-editor-image-wrap-uploading", uploading)
|
|
const overlay = wrapper.querySelector<HTMLElement>(".agent-desk-editor-image-loading")
|
|
if (overlay) {
|
|
overlay.classList.toggle("hidden", !uploading)
|
|
overlay.classList.toggle("flex", uploading)
|
|
}
|
|
}
|