feat: editor advanced options & disable AI summary (#600)
This commit is contained in:
parent
5dcbc24db9
commit
8ef36c065b
|
|
@ -32,6 +32,7 @@ import { FieldLabel } from "~/components/ui/FieldLabel"
|
|||
import { ImageUploader } from "~/components/ui/ImageUploader"
|
||||
import { Input } from "~/components/ui/Input"
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
import { Switch } from "~/components/ui/Switch"
|
||||
import { TagInput } from "~/components/ui/TagInput"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { toolbars } from "~/editor"
|
||||
|
|
@ -242,6 +243,7 @@ export default function SubdomainEditor() {
|
|||
applications: page.data?.metadata?.content?.sources,
|
||||
characterId: site.data?.characterId,
|
||||
cover: values.cover,
|
||||
disableAISummary: values.disableAISummary,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -301,6 +303,7 @@ export default function SubdomainEditor() {
|
|||
address: "",
|
||||
mime_type: "",
|
||||
},
|
||||
disableAISummary: page.data.metadata?.content?.disableAISummary,
|
||||
})
|
||||
setDefaultSlug(
|
||||
getDefaultSlug(
|
||||
|
|
@ -465,6 +468,9 @@ export default function SubdomainEditor() {
|
|||
)
|
||||
}, [draftKey, subdomain, site.data?.characterId])
|
||||
|
||||
const [isAdvancedOptionsOpen, setIsAdvancedOptionsOpen] =
|
||||
useState<boolean>(false)
|
||||
|
||||
const extraProperties = (
|
||||
<EditorExtraProperties
|
||||
defaultSlug={defaultSlug}
|
||||
|
|
@ -472,6 +478,7 @@ export default function SubdomainEditor() {
|
|||
isPost={isPost}
|
||||
subdomain={subdomain}
|
||||
userTags={userTags}
|
||||
openAdvancedOptions={() => setIsAdvancedOptionsOpen(true)}
|
||||
/>
|
||||
)
|
||||
|
||||
|
|
@ -650,12 +657,18 @@ export default function SubdomainEditor() {
|
|||
isPost={isPost}
|
||||
userTags={userTags}
|
||||
subdomain={subdomain}
|
||||
openAdvancedOptions={() => setIsAdvancedOptionsOpen(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DashboardMain>
|
||||
<EditorAdvancedOptions
|
||||
updateValue={updateValue}
|
||||
isAdvancedOptionsOpen={isAdvancedOptionsOpen}
|
||||
setIsAdvancedOptionsOpen={setIsAdvancedOptionsOpen}
|
||||
/>
|
||||
<Modal
|
||||
open={isCheersOpen}
|
||||
setOpen={setIsCheersOpen}
|
||||
|
|
@ -723,154 +736,219 @@ const EditorExtraProperties: FC<{
|
|||
subdomain: string
|
||||
defaultSlug: string
|
||||
userTags: string[]
|
||||
}> = memo(({ isPost, updateValue, subdomain, defaultSlug, userTags }) => {
|
||||
const values = useEditorState(
|
||||
(state) => pick(state, ["publishedAt", "slug", "excerpt", "tags", "cover"]),
|
||||
shallow,
|
||||
)
|
||||
const { t } = useTranslation("dashboard")
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto w-[280px] border-l bg-zinc-50 p-5 space-y-5">
|
||||
<div>
|
||||
<label className="form-label" htmlFor="publishAt">
|
||||
{t("Publish at")}
|
||||
</label>
|
||||
<DateInput
|
||||
className=""
|
||||
allowDeselect
|
||||
clearable
|
||||
valueFormat="YYYY-MM-DD, h:mm a"
|
||||
name="publishAt"
|
||||
id="publishAt"
|
||||
value={values.publishedAt ? new Date(values.publishedAt) : undefined}
|
||||
onChange={(value: Date | null) => {
|
||||
if (value) {
|
||||
updateValue("publishedAt", value.toISOString())
|
||||
} else {
|
||||
updateValue("publishedAt", "")
|
||||
openAdvancedOptions: () => void
|
||||
}> = memo(
|
||||
({
|
||||
isPost,
|
||||
updateValue,
|
||||
subdomain,
|
||||
defaultSlug,
|
||||
userTags,
|
||||
openAdvancedOptions,
|
||||
}) => {
|
||||
const values = useEditorState(
|
||||
(state) =>
|
||||
pick(state, ["publishedAt", "slug", "excerpt", "tags", "cover"]),
|
||||
shallow,
|
||||
)
|
||||
const { t } = useTranslation("dashboard")
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto w-[280px] border-l bg-zinc-50 p-5 space-y-5">
|
||||
<div>
|
||||
<label className="form-label" htmlFor="publishAt">
|
||||
{t("Publish at")}
|
||||
</label>
|
||||
<DateInput
|
||||
className=""
|
||||
allowDeselect
|
||||
clearable
|
||||
valueFormat="YYYY-MM-DD, h:mm a"
|
||||
name="publishAt"
|
||||
id="publishAt"
|
||||
value={
|
||||
values.publishedAt ? new Date(values.publishedAt) : undefined
|
||||
}
|
||||
}}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: "0.5rem",
|
||||
borderColor: "var(--border-color)",
|
||||
height: "2.5rem",
|
||||
"&:focus-within": {
|
||||
borderColor: "var(--theme-color)",
|
||||
onChange={(value: Date | null) => {
|
||||
if (value) {
|
||||
updateValue("publishedAt", value.toISOString())
|
||||
} else {
|
||||
updateValue("publishedAt", "")
|
||||
}
|
||||
}}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: "0.5rem",
|
||||
borderColor: "var(--border-color)",
|
||||
height: "2.5rem",
|
||||
"&:focus-within": {
|
||||
borderColor: "var(--theme-color)",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{t(
|
||||
`This ${
|
||||
isPost ? "post" : "page"
|
||||
} will be accessible from this time. Leave blank to use the current time.`,
|
||||
)}
|
||||
</div>
|
||||
{values.publishedAt > new Date().toISOString() && (
|
||||
<div className="text-xs mt-1 text-red-500">
|
||||
}}
|
||||
/>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{t(
|
||||
"The post is currently unavailable as its publication date has been scheduled for a future time.",
|
||||
`This ${
|
||||
isPost ? "post" : "page"
|
||||
} will be accessible from this time. Leave blank to use the current time.`,
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
name="slug"
|
||||
value={values.slug}
|
||||
placeholder={defaultSlug}
|
||||
label={t(`${isPost ? "Post" : "Page"} slug`) || ""}
|
||||
id="slug"
|
||||
isBlock
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
updateValue("slug", e.target.value)
|
||||
}
|
||||
help={
|
||||
<>
|
||||
{(values.slug || defaultSlug) && (
|
||||
<>
|
||||
{t(`This ${isPost ? "post" : "page"} will be accessible at`)}{" "}
|
||||
<UniLink
|
||||
href={`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
noProtocol: true,
|
||||
})}
|
||||
/{encodeURIComponent(values.slug || defaultSlug)}
|
||||
</UniLink>
|
||||
</>
|
||||
{values.publishedAt > new Date().toISOString() && (
|
||||
<div className="text-xs mt-1 text-red-500">
|
||||
{t(
|
||||
"The post is currently unavailable as its publication date has been scheduled for a future time.",
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
name="tags"
|
||||
value={values.tags}
|
||||
label={t("Tags") || ""}
|
||||
id="tags"
|
||||
isBlock
|
||||
renderInput={(props) => (
|
||||
<TagInput
|
||||
{...props}
|
||||
userTags={userTags}
|
||||
onTagChange={(value: string) => updateValue("tags", value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label={t("Excerpt") || ""}
|
||||
isBlock
|
||||
name="excerpt"
|
||||
id="excerpt"
|
||||
value={values.excerpt}
|
||||
multiline
|
||||
rows={4}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateValue("excerpt", e.target.value)
|
||||
}}
|
||||
help={t("Leave it blank to use auto-generated excerpt")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel label={t("Cover Image")} />
|
||||
<ImageUploader
|
||||
id="icon"
|
||||
className="aspect-video rounded-lg"
|
||||
value={values.cover as any}
|
||||
hasClose={true}
|
||||
withMimeType={true}
|
||||
uploadEnd={(key) => {
|
||||
const { address, mime_type } = key as {
|
||||
address?: string
|
||||
mime_type?: string
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
name="slug"
|
||||
value={values.slug}
|
||||
placeholder={defaultSlug}
|
||||
label={t(`${isPost ? "Post" : "Page"} slug`) || ""}
|
||||
id="slug"
|
||||
isBlock
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
updateValue("slug", e.target.value)
|
||||
}
|
||||
updateValue("cover", {
|
||||
address,
|
||||
mime_type,
|
||||
})
|
||||
}}
|
||||
accept="image/*"
|
||||
/>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{t("Leave blank to use the first image in the post")}
|
||||
help={
|
||||
<>
|
||||
{(values.slug || defaultSlug) && (
|
||||
<>
|
||||
{t(
|
||||
`This ${isPost ? "post" : "page"} will be accessible at`,
|
||||
)}{" "}
|
||||
<UniLink
|
||||
href={`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
noProtocol: true,
|
||||
})}
|
||||
/{encodeURIComponent(values.slug || defaultSlug)}
|
||||
</UniLink>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
name="tags"
|
||||
value={values.tags}
|
||||
label={t("Tags") || ""}
|
||||
id="tags"
|
||||
isBlock
|
||||
renderInput={(props) => (
|
||||
<TagInput
|
||||
{...props}
|
||||
userTags={userTags}
|
||||
onTagChange={(value: string) => updateValue("tags", value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label={t("Excerpt") || ""}
|
||||
isBlock
|
||||
name="excerpt"
|
||||
id="excerpt"
|
||||
value={values.excerpt}
|
||||
multiline
|
||||
rows={4}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateValue("excerpt", e.target.value)
|
||||
}}
|
||||
help={t("Leave it blank to use auto-generated excerpt")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel label={t("Cover Image")} />
|
||||
<ImageUploader
|
||||
id="icon"
|
||||
className="aspect-video rounded-lg"
|
||||
value={values.cover as any}
|
||||
hasClose={true}
|
||||
withMimeType={true}
|
||||
uploadEnd={(key) => {
|
||||
const { address, mime_type } = key as {
|
||||
address?: string
|
||||
mime_type?: string
|
||||
}
|
||||
updateValue("cover", {
|
||||
address,
|
||||
mime_type,
|
||||
})
|
||||
}}
|
||||
accept="image/*"
|
||||
/>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{t("Leave blank to use the first image in the post")}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="border"
|
||||
type="button"
|
||||
isBlock
|
||||
onClick={openAdvancedOptions}
|
||||
>
|
||||
<span className="inline-flex items-center">
|
||||
<i className="icon-[mingcute--settings-4-fill] inline-block mr-2" />
|
||||
<span>{t("Advanced Settings")}</span>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
EditorExtraProperties.displayName = "EditorExtraProperties"
|
||||
|
||||
const EditorAdvancedOptions: FC<{
|
||||
updateValue: <K extends keyof Values>(key: K, value: Values[K]) => void
|
||||
|
||||
isAdvancedOptionsOpen: boolean
|
||||
setIsAdvancedOptionsOpen: (state: boolean) => void
|
||||
}> = memo(
|
||||
({ updateValue, isAdvancedOptionsOpen, setIsAdvancedOptionsOpen }) => {
|
||||
const values = useEditorState(
|
||||
(state) => pick(state, ["disableAISummary"]),
|
||||
shallow,
|
||||
)
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isAdvancedOptionsOpen}
|
||||
setOpen={setIsAdvancedOptionsOpen}
|
||||
title={t("Advanced Settings")}
|
||||
>
|
||||
<div className="p-5 space-y-3">
|
||||
<div>
|
||||
<Switch
|
||||
label={t("Disable AI-generated summary")}
|
||||
checked={values.disableAISummary}
|
||||
setChecked={(state) => updateValue("disableAISummary", state)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
EditorAdvancedOptions.displayName = "EditorAdvancedOptions"
|
||||
|
|
|
|||
|
|
@ -17,10 +17,14 @@ export default async function PostMeta({
|
|||
}) {
|
||||
const { t } = await useTranslation("common")
|
||||
const { i18n } = await useTranslation()
|
||||
const summary = await getSummary({
|
||||
cid: toCid(page.metadata?.uri || ""),
|
||||
lang: i18n.resolvedLanguage,
|
||||
})
|
||||
|
||||
let summary: string | undefined
|
||||
if (!page.metadata.content.disableAISummary) {
|
||||
summary = await getSummary({
|
||||
cid: toCid(page.metadata?.uri || ""),
|
||||
lang: i18n.resolvedLanguage,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="xlog-post-meta">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { Fragment } from "react"
|
||||
|
||||
import { Switch as HUISwitch } from "@headlessui/react"
|
||||
|
||||
interface SwitchProps {
|
||||
label: string
|
||||
checked: boolean
|
||||
setChecked: (state: boolean) => void
|
||||
}
|
||||
export const Switch = ({ label, checked, setChecked }: SwitchProps) => (
|
||||
<HUISwitch.Group>
|
||||
<div className="flex items-center">
|
||||
{/*Switch self*/}
|
||||
<HUISwitch checked={checked} onChange={setChecked} as={Fragment}>
|
||||
{({ checked }) => (
|
||||
<button
|
||||
className={`${
|
||||
checked ? "bg-[var(--theme-color)]" : "bg-gray-200"
|
||||
} relative inline-flex h-6 w-11 items-center rounded-full`}
|
||||
>
|
||||
<span className="sr-only">{label}</span>
|
||||
<span
|
||||
className={`${
|
||||
checked ? "translate-x-6" : "translate-x-1"
|
||||
} inline-block h-4 w-4 transform rounded-full bg-white transition`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</HUISwitch>
|
||||
|
||||
{/*Switch label*/}
|
||||
<HUISwitch.Label className={"ml-4"}>{label}</HUISwitch.Label>
|
||||
</div>
|
||||
</HUISwitch.Group>
|
||||
)
|
||||
|
|
@ -12,6 +12,7 @@ export const initialEditorState = {
|
|||
address: "",
|
||||
mime_type: "",
|
||||
},
|
||||
disableAISummary: false,
|
||||
}
|
||||
export type Values = {
|
||||
title: string
|
||||
|
|
@ -25,6 +26,7 @@ export type Values = {
|
|||
address?: string
|
||||
mime_type?: string
|
||||
}
|
||||
disableAISummary: boolean
|
||||
}
|
||||
|
||||
export const useEditorState = create<
|
||||
|
|
|
|||
|
|
@ -65,6 +65,12 @@ export const expandCrossbellNote = async ({
|
|||
expandedNote.metadata.content.date_published = expandedNote.createdAt
|
||||
}
|
||||
|
||||
expandedNote.metadata.content.disableAISummary = Boolean(
|
||||
expandedNote.metadata.content.attributes?.find(
|
||||
(attribute) => attribute.trait_type === "xlog_disable_ai_summary",
|
||||
)?.value,
|
||||
)
|
||||
|
||||
if (useStat) {
|
||||
const stat = await (
|
||||
await fetch(
|
||||
|
|
|
|||
|
|
@ -122,5 +122,7 @@
|
|||
"Operators have permissions to enter your dashboard, change your settings(excluding xLog subdomain) and post, modify, delete contents on your site.": "シャドー認証では、ダッシュボードへのアクセス、設定の変更(xLog サブドメインを除く)、サイトのコンテンツの公開・修正・削除が可能です。",
|
||||
"Add": "新規",
|
||||
"delete_confirmation_post": "この記事を削除してもよろしいですか?",
|
||||
"delete_confirmation_page": "このページを削除してもよろしいですか?"
|
||||
}
|
||||
"delete_confirmation_page": "このページを削除してもよろしいですか?",
|
||||
"Advanced Settings": "詳細設定",
|
||||
"Disable AI-generated summary": "AIが生成した要約を無効化する"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,5 +200,7 @@
|
|||
"Help: Markdown syntax and components used by xLog": "幫助:xLog 使用的 Markdown 語法和組件",
|
||||
"Align Center": "居中",
|
||||
"Align Right": "靠右",
|
||||
"Formula": "公式"
|
||||
}
|
||||
"Formula": "公式",
|
||||
"Advanced Settings": "高級設置",
|
||||
"Disable AI-generated summary": "禁用AI生成的摘要"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -211,5 +211,7 @@
|
|||
"Help: Markdown syntax and components used by xLog": "帮助:xLog 使用的 Markdown 语法和组件",
|
||||
"Align Center": "居中对齐",
|
||||
"Align Right": "右对齐",
|
||||
"Formula": "公式"
|
||||
}
|
||||
"Formula": "公式",
|
||||
"Advanced Settings": "高级设置",
|
||||
"Disable AI-generated summary": "禁用AI生成的摘要"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export type ExpandedNote = NoteEntity & {
|
|||
reason?: string
|
||||
}
|
||||
contentHTML?: string
|
||||
disableAISummary?: boolean
|
||||
}
|
||||
}
|
||||
stat?: {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export async function createOrUpdatePage(
|
|||
address?: string
|
||||
mime_type?: string
|
||||
}
|
||||
disableAISummary?: boolean
|
||||
},
|
||||
customUnidata?: Unidata,
|
||||
newbieToken?: string,
|
||||
|
|
@ -118,14 +119,24 @@ export async function createOrUpdatePage(
|
|||
"xlog",
|
||||
...(input.applications?.filter((app) => app !== "xlog") || []),
|
||||
],
|
||||
...(input.slug && {
|
||||
attributes: [
|
||||
{
|
||||
trait_type: "xlog_slug",
|
||||
value: input.slug,
|
||||
},
|
||||
],
|
||||
}),
|
||||
attributes: [
|
||||
...(input.slug
|
||||
? [
|
||||
{
|
||||
trait_type: "xlog_slug",
|
||||
value: input.slug,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(input.disableAISummary
|
||||
? [
|
||||
{
|
||||
trait_type: "xlog_disable_ai_summary",
|
||||
value: input.disableAISummary,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
attachments: input.cover?.address
|
||||
? [
|
||||
{
|
||||
|
|
@ -215,6 +226,7 @@ const getLocalPages = (input: {
|
|||
],
|
||||
slug: page.values?.slug,
|
||||
sources: ["xlog"],
|
||||
disableAISummary: page.values?.disableAISummary,
|
||||
},
|
||||
},
|
||||
local: true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue