feat: editor advanced options & disable AI summary (#600)

This commit is contained in:
Nya Candy 2023-06-08 18:13:39 +08:00 committed by GitHub
parent 5dcbc24db9
commit 8ef36c065b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 300 additions and 156 deletions

View File

@ -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"

View File

@ -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">

View File

@ -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>
)

View File

@ -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<

View File

@ -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(

View File

@ -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が生成した要約を無効化する"
}

View File

@ -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生成的摘要"
}

View File

@ -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生成的摘要"
}

View File

@ -87,6 +87,7 @@ export type ExpandedNote = NoteEntity & {
reason?: string
}
contentHTML?: string
disableAISummary?: boolean
}
}
stat?: {

View File

@ -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,