mirror of
https://github.com/coaidev/coai.git
synced 2025-05-19 04:50:14 +09:00
feat: support custom pwa manifest
This commit is contained in:
parent
5366fb7307
commit
3335e81c1e
@ -14,6 +14,7 @@ export type GeneralState = {
|
||||
backend: string;
|
||||
docs: string;
|
||||
file: string;
|
||||
pwa_manifest: string;
|
||||
};
|
||||
|
||||
export type MailState = {
|
||||
@ -117,6 +118,7 @@ export const initialSystemState: SystemProps = {
|
||||
backend: "",
|
||||
docs: "",
|
||||
file: "",
|
||||
pwa_manifest: "",
|
||||
},
|
||||
site: {
|
||||
relay_plan: false,
|
||||
|
@ -118,6 +118,10 @@
|
||||
--assistant-border-hover: hsla(218, 100%, 64%, .25);
|
||||
--assistant-shadow: hsla(218, 100%, 64%, .05);
|
||||
}
|
||||
|
||||
[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus {
|
||||
@apply border-border;
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
|
@ -371,3 +371,7 @@ input[type="number"] {
|
||||
.text-common {
|
||||
color: hsl(var(--text)) !important;
|
||||
}
|
||||
|
||||
.error-border {
|
||||
border-color: hsl(var(--destructive)) !important;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
|
||||
import "@/assets/common/editor.less";
|
||||
import { Textarea } from "./ui/textarea.tsx";
|
||||
import Markdown from "./Markdown.tsx";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Toggle } from "./ui/toggle.tsx";
|
||||
import { mobile } from "@/utils/device.ts";
|
||||
import { Button } from "./ui/button.tsx";
|
||||
@ -23,6 +23,8 @@ type RichEditorProps = {
|
||||
onChange: (value: string) => void;
|
||||
maxLength?: number;
|
||||
|
||||
formatter?: (value: string) => string;
|
||||
isInvalid?: (value: string) => boolean;
|
||||
title?: string;
|
||||
|
||||
open?: boolean;
|
||||
@ -38,7 +40,9 @@ function RichEditor({
|
||||
value,
|
||||
onChange,
|
||||
maxLength,
|
||||
formatter,
|
||||
submittable,
|
||||
isInvalid,
|
||||
onSubmit,
|
||||
setOpen,
|
||||
closeOnSubmit,
|
||||
@ -48,6 +52,13 @@ function RichEditor({
|
||||
const [openPreview, setOpenPreview] = useState(!mobile);
|
||||
const [openInput, setOpenInput] = useState(true);
|
||||
|
||||
const formattedValue = useMemo(() => {
|
||||
return formatter ? formatter(value) : value;
|
||||
}, [value, formatter]);
|
||||
const invalid = useMemo(() => {
|
||||
return isInvalid ? isInvalid(value) : false;
|
||||
}, [value, isInvalid]);
|
||||
|
||||
const handler = () => {
|
||||
if (!input.current) return;
|
||||
const target = input.current as HTMLElement;
|
||||
@ -133,7 +144,10 @@ function RichEditor({
|
||||
<Textarea
|
||||
placeholder={t("chat.placeholder-raw")}
|
||||
value={value}
|
||||
className={`editor-input`}
|
||||
className={cn(
|
||||
`editor-input transition-all`,
|
||||
invalid && `error-border`,
|
||||
)}
|
||||
id={`editor`}
|
||||
maxLength={maxLength}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
@ -141,7 +155,7 @@ function RichEditor({
|
||||
/>
|
||||
)}
|
||||
{openPreview && (
|
||||
<Markdown className={`editor-preview`} children={value} />
|
||||
<Markdown className={`editor-preview`} children={formattedValue} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -198,3 +212,20 @@ function EditorProvider(props: RichEditorProps) {
|
||||
}
|
||||
|
||||
export default EditorProvider;
|
||||
|
||||
export function JSONEditorProvider({ ...props }: RichEditorProps) {
|
||||
return (
|
||||
<EditorProvider
|
||||
{...props}
|
||||
formatter={(value) => `\`\`\`json\n${value}\n\`\`\``}
|
||||
isInvalid={(value) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ import {
|
||||
} from "@/components/ui/dialog.tsx";
|
||||
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||
import Require from "@/components/Require.tsx";
|
||||
import { Loader2, Settings2 } from "lucide-react";
|
||||
import { Loader2, PencilLine, Settings2 } from "lucide-react";
|
||||
import { FlexibleTextarea } from "@/components/ui/textarea.tsx";
|
||||
import Tips from "@/components/Tips.tsx";
|
||||
import { cn } from "@/components/ui/lib/utils.ts";
|
||||
@ -54,6 +54,7 @@ import { allGroups } from "@/utils/groups.ts";
|
||||
import { useChannelModels } from "@/admin/hook.tsx";
|
||||
import { useSelector } from "react-redux";
|
||||
import { selectSupportModels } from "@/store/chat.ts";
|
||||
import { JSONEditorProvider } from "@/components/EditorProvider.tsx";
|
||||
|
||||
type CompProps<T> = {
|
||||
data: T;
|
||||
@ -222,6 +223,20 @@ function General({ data, dispatch, onChange }: CompProps<GeneralState>) {
|
||||
/>
|
||||
</ParagraphItem>
|
||||
<ParagraphDescription>{t("admin.system.fileTip")}</ParagraphDescription>
|
||||
<ParagraphItem>
|
||||
<Label>PWA Manifest</Label>
|
||||
<JSONEditorProvider
|
||||
value={data.pwa_manifest ?? ""}
|
||||
onChange={(value) =>
|
||||
dispatch({ type: "update:general.pwa_manifest", value })
|
||||
}
|
||||
>
|
||||
<Button variant={`outline`}>
|
||||
<PencilLine className={`h-4 w-4 mr-1`} />
|
||||
{t("edit")}
|
||||
</Button>
|
||||
</JSONEditorProvider>
|
||||
</ParagraphItem>
|
||||
<ParagraphFooter>
|
||||
<div className={`grow`} />
|
||||
<RootDialog />
|
||||
|
@ -25,11 +25,12 @@ type ApiInfo struct {
|
||||
}
|
||||
|
||||
type generalState struct {
|
||||
Title string `json:"title" mapstructure:"title"`
|
||||
Logo string `json:"logo" mapstructure:"logo"`
|
||||
Backend string `json:"backend" mapstructure:"backend"`
|
||||
File string `json:"file" mapstructure:"file"`
|
||||
Docs string `json:"docs" mapstructure:"docs"`
|
||||
Title string `json:"title" mapstructure:"title"`
|
||||
Logo string `json:"logo" mapstructure:"logo"`
|
||||
Backend string `json:"backend" mapstructure:"backend"`
|
||||
File string `json:"file" mapstructure:"file"`
|
||||
Docs string `json:"docs" mapstructure:"docs"`
|
||||
PWAManifest string `json:"pwa_manifest" mapstructure:"pwamanifest"`
|
||||
}
|
||||
|
||||
type siteState struct {
|
||||
@ -98,6 +99,10 @@ func (c *SystemConfig) Load() {
|
||||
|
||||
globals.CacheAcceptedExpire = c.GetCacheAcceptedExpire()
|
||||
globals.CacheAcceptedSize = c.GetCacheAcceptedSize()
|
||||
|
||||
if c.General.PWAManifest == "" {
|
||||
c.General.PWAManifest = utils.ReadPWAManifest()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SystemConfig) SaveConfig() error {
|
||||
@ -133,6 +138,7 @@ func (c *SystemConfig) UpdateConfig(data *SystemConfig) error {
|
||||
c.Common = data.Common
|
||||
|
||||
utils.ApplySeo(c.General.Title, c.General.Logo)
|
||||
utils.ApplyPWAManifest(c.General.PWAManifest)
|
||||
|
||||
return c.SaveConfig()
|
||||
}
|
||||
|
@ -76,6 +76,51 @@ func ApplySeo(title, icon string) {
|
||||
globals.Info("[service] seo optimization applied to index.cache.html")
|
||||
}
|
||||
|
||||
func ApplyPWAManifest(content string) {
|
||||
// pwa manifest rewrite (site.webmanifest -> site.cache.webmanifest)
|
||||
|
||||
if !viper.GetBool("serve_static") {
|
||||
return
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
// read from site.webmanifest if not provided
|
||||
|
||||
var err error
|
||||
content, err = ReadFile("./app/dist/site.webmanifest")
|
||||
if err != nil {
|
||||
globals.Warn(fmt.Sprintf("[service] failed to read site.webmanifest: %s", err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := WriteFile("./app/dist/site.cache.webmanifest", content, true); err != nil {
|
||||
globals.Warn(fmt.Sprintf("[service] failed to write site.cache.webmanifest: %s", err.Error()))
|
||||
}
|
||||
|
||||
globals.Info("[service] pwa manifest applied to site.cache.webmanifest")
|
||||
}
|
||||
|
||||
func ReadPWAManifest() (content string) {
|
||||
// read site.cache.webmanifest content or site.webmanifest if not found
|
||||
|
||||
if !viper.GetBool("serve_static") {
|
||||
return
|
||||
}
|
||||
|
||||
if text, err := ReadFile("./app/dist/site.cache.webmanifest"); err == nil && len(text) > 0 {
|
||||
return text
|
||||
}
|
||||
|
||||
if text, err := ReadFile("./app/dist/site.webmanifest"); err != nil {
|
||||
globals.Warn(fmt.Sprintf("[service] failed to read site.webmanifest: %s", err.Error()))
|
||||
} else {
|
||||
content = text
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func RegisterStaticRoute(engine *gin.Engine) {
|
||||
// static files are in ~/app/dist
|
||||
|
||||
@ -92,11 +137,16 @@ func RegisterStaticRoute(engine *gin.Engine) {
|
||||
}
|
||||
|
||||
ApplySeo(viper.GetString("system.general.title"), viper.GetString("system.general.logo"))
|
||||
ApplyPWAManifest(viper.GetString("system.general.pwamanifest"))
|
||||
|
||||
// serve / -> index.cache.html
|
||||
engine.GET("/", func(c *gin.Context) {
|
||||
c.File("./app/dist/index.cache.html")
|
||||
})
|
||||
|
||||
engine.GET("/site.webmanifest", func(c *gin.Context) {
|
||||
c.File("./app/dist/site.cache.webmanifest")
|
||||
})
|
||||
|
||||
engine.Use(static.Serve("/", static.LocalFile("./app/dist", true)))
|
||||
engine.NoRoute(func(c *gin.Context) {
|
||||
c.File("./app/dist/index.cache.html")
|
||||
|
Loading…
Reference in New Issue
Block a user