Commit b332ed1b authored by Gillian97's avatar Gillian97

feat: app card view opt

parent 5d783a49
...@@ -4,13 +4,7 @@ import { ...@@ -4,13 +4,7 @@ import {
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import Tooltip from '../base/tooltip' import Tooltip from '../base/tooltip'
import AppIcon from '../base/app-icon' import AppIcon from '../base/app-icon'
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_' import { randomString } from '@/utils'
export function randomString(length: number) {
let result = ''
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]
return result
}
export type IAppBasicProps = { export type IAppBasicProps = {
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion' iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
......
...@@ -6,7 +6,6 @@ import { useContext } from 'use-context-selector' ...@@ -6,7 +6,6 @@ import { useContext } from 'use-context-selector'
import { UserCircleIcon } from '@heroicons/react/24/solid' import { UserCircleIcon } from '@heroicons/react/24/solid'
import cn from 'classnames' import cn from 'classnames'
import type { DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc, ThoughtItem } from '../type' import type { DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc, ThoughtItem } from '../type'
import { randomString } from '../../../app-sidebar/basic'
import OperationBtn from '../operation' import OperationBtn from '../operation'
import LoadingAnim from '../loading-anim' import LoadingAnim from '../loading-anim'
import { EditIcon, EditIconSolid, OpeningStatementIcon, RatingIcon } from '../icon-component' import { EditIcon, EditIconSolid, OpeningStatementIcon, RatingIcon } from '../icon-component'
...@@ -14,6 +13,7 @@ import s from '../style.module.css' ...@@ -14,6 +13,7 @@ import s from '../style.module.css'
import MoreInfo from '../more-info' import MoreInfo from '../more-info'
import CopyBtn from '../copy-btn' import CopyBtn from '../copy-btn'
import Thought from '../thought' import Thought from '../thought'
import { randomString } from '@/utils'
import type { Annotation, MessageRating } from '@/models/log' import type { Annotation, MessageRating } from '@/models/log'
import AppContext from '@/context/app-context' import AppContext from '@/context/app-context'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
......
...@@ -16,8 +16,8 @@ import dayjs from 'dayjs' ...@@ -16,8 +16,8 @@ import dayjs from 'dayjs'
import { createContext, useContext } from 'use-context-selector' import { createContext, useContext } from 'use-context-selector'
import classNames from 'classnames' import classNames from 'classnames'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { randomString } from '../../app-sidebar/basic'
import s from './style.module.css' import s from './style.module.css'
import { randomString } from '@/utils'
import { EditIconSolid } from '@/app/components/app/chat/icon-component' import { EditIconSolid } from '@/app/components/app/chat/icon-component'
import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type' import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type'
import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse } from '@/models/log' import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse } from '@/models/log'
......
...@@ -4,29 +4,30 @@ import React, { useState } from 'react' ...@@ -4,29 +4,30 @@ import React, { useState } from 'react'
import { import {
Cog8ToothIcon, Cog8ToothIcon,
DocumentTextIcon, DocumentTextIcon,
PaintBrushIcon,
RocketLaunchIcon, RocketLaunchIcon,
ShareIcon,
} from '@heroicons/react/24/outline' } from '@heroicons/react/24/outline'
import { SparklesIcon } from '@heroicons/react/24/solid'
import { usePathname, useRouter } from 'next/navigation' import { usePathname, useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import SettingsModal from './settings' import SettingsModal from './settings'
import ShareLink from './share-link'
import EmbeddedModal from './embedded' import EmbeddedModal from './embedded'
import CustomizeModal from './customize' import CustomizeModal from './customize'
import style from './style.module.css'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import AppBasic, { randomString } from '@/app/components/app-sidebar/basic' import AppBasic from '@/app/components/app-sidebar/basic'
import { asyncRunSafe, randomString } from '@/utils'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Tag from '@/app/components/base/tag' import Tag from '@/app/components/base/tag'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider'
import CopyFeedback from '@/app/components/base/copy-feedback'
import type { AppDetailResponse } from '@/models/app' import type { AppDetailResponse } from '@/models/app'
import './style.css'
import { AppType } from '@/types/app' import { AppType } from '@/types/app'
export type IAppCardProps = { export type IAppCardProps = {
className?: string className?: string
appInfo: AppDetailResponse appInfo: AppDetailResponse
cardType?: 'app' | 'api' | 'webapp' cardType?: 'api' | 'webapp'
customBgColor?: string customBgColor?: string
onChangeStatus: (val: boolean) => Promise<any> onChangeStatus: (val: boolean) => Promise<any>
onSaveSiteConfig?: (params: any) => Promise<any> onSaveSiteConfig?: (params: any) => Promise<any>
...@@ -34,12 +35,12 @@ export type IAppCardProps = { ...@@ -34,12 +35,12 @@ export type IAppCardProps = {
} }
const EmbedIcon: FC<{ className?: string }> = ({ className = '' }) => { const EmbedIcon: FC<{ className?: string }> = ({ className = '' }) => {
return <div className={`codeBrowserIcon ${className}`}></div> return <div className={`${style.codeBrowserIcon} ${className}`}></div>
} }
function AppCard({ function AppCard({
appInfo, appInfo,
cardType = 'app', cardType = 'webapp',
customBgColor, customBgColor,
onChangeStatus, onChangeStatus,
onSaveSiteConfig, onSaveSiteConfig,
...@@ -49,24 +50,45 @@ function AppCard({ ...@@ -49,24 +50,45 @@ function AppCard({
const router = useRouter() const router = useRouter()
const pathname = usePathname() const pathname = usePathname()
const [showSettingsModal, setShowSettingsModal] = useState(false) const [showSettingsModal, setShowSettingsModal] = useState(false)
const [showShareModal, setShowShareModal] = useState(false)
const [showEmbedded, setShowEmbedded] = useState(false) const [showEmbedded, setShowEmbedded] = useState(false)
const [showCustomizeModal, setShowCustomizeModal] = useState(false) const [showCustomizeModal, setShowCustomizeModal] = useState(false)
const [genLoading, setGenLoading] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
const OPERATIONS_MAP = { const OPERATIONS_MAP = {
webapp: [ webapp: [
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon }, {
{ opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon }, opName: t('appOverview.overview.appInfo.preview'),
appInfo.mode === AppType.chat ? { opName: t('appOverview.overview.appInfo.embedded.entry'), opIcon: EmbedIcon } : false, opIcon: RocketLaunchIcon,
{ opName: t('appOverview.overview.appInfo.settings.entry'), opIcon: Cog8ToothIcon }, },
appInfo.mode === AppType.chat
? {
opName: t('appOverview.overview.appInfo.embedded.entry'),
opIcon: EmbedIcon,
}
: false,
{
opName: t('appOverview.overview.appInfo.customize.entry'),
opIcon: PaintBrushIcon,
},
{
opName: t('appOverview.overview.appInfo.settings.entry'),
opIcon: Cog8ToothIcon,
},
].filter(item => !!item), ].filter(item => !!item),
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }], api: [
{
opName: t('appOverview.overview.apiInfo.doc'),
opIcon: DocumentTextIcon,
},
],
app: [], app: [],
} }
const isApp = cardType === 'app' || cardType === 'webapp' const isApp = cardType === 'webapp'
const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title') const basicName = isApp
? appInfo?.site?.title
: t('appOverview.overview.apiInfo.title')
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
const { app_base_url, access_token } = appInfo.site ?? {} const { app_base_url, access_token } = appInfo.site ?? {}
const appUrl = `${app_base_url}/${appInfo.mode}/${access_token}` const appUrl = `${app_base_url}/${appInfo.mode}/${access_token}`
...@@ -82,9 +104,9 @@ function AppCard({ ...@@ -82,9 +104,9 @@ function AppCard({
return () => { return () => {
window.open(appUrl, '_blank') window.open(appUrl, '_blank')
} }
case t('appOverview.overview.appInfo.share.entry'): case t('appOverview.overview.appInfo.customize.entry'):
return () => { return () => {
setShowShareModal(true) setShowCustomizeModal(true)
} }
case t('appOverview.overview.appInfo.settings.entry'): case t('appOverview.overview.appInfo.settings.entry'):
return () => { return () => {
...@@ -104,13 +126,17 @@ function AppCard({ ...@@ -104,13 +126,17 @@ function AppCard({
} }
} }
const onClickCustomize = () => { const onGenCode = async () => {
setShowCustomizeModal(true) setGenLoading(true)
await asyncRunSafe(onGenerateCode?.() as any)
setGenLoading(false)
} }
return ( return (
<div <div
className={`flex flex-col w-full shadow-sm border-[0.5px] rounded-lg border-gray-200 ${className ?? ''}`} className={`flex flex-col w-full shadow-sm border-[0.5px] rounded-lg border-gray-200 ${
className ?? ''
}`}
> >
<div className={`px-6 py-4 ${customBgColor ?? bgColor} rounded-lg`}> <div className={`px-6 py-4 ${customBgColor ?? bgColor} rounded-lg`}>
<div className="mb-2.5 flex flex-row items-start justify-between"> <div className="mb-2.5 flex flex-row items-start justify-between">
...@@ -127,7 +153,9 @@ function AppCard({ ...@@ -127,7 +153,9 @@ function AppCard({
/> />
<div className="flex flex-row items-center h-9"> <div className="flex flex-row items-center h-9">
<Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}> <Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}>
{runningStatus ? t('appOverview.overview.status.running') : t('appOverview.overview.status.disable')} {runningStatus
? t('appOverview.overview.status.running')
: t('appOverview.overview.status.disable')}
</Tag> </Tag>
<Switch defaultValue={runningStatus} onChange={onChangeStatus} /> <Switch defaultValue={runningStatus} onChange={onChangeStatus} />
</div> </div>
...@@ -135,35 +163,62 @@ function AppCard({ ...@@ -135,35 +163,62 @@ function AppCard({
<div className="flex flex-col justify-center py-2"> <div className="flex flex-col justify-center py-2">
<div className="py-1"> <div className="py-1">
<div className="pb-1 text-xs text-gray-500"> <div className="pb-1 text-xs text-gray-500">
{isApp ? t('appOverview.overview.appInfo.accessibleAddress') : t('appOverview.overview.apiInfo.accessibleAddress')} {isApp
? t('appOverview.overview.appInfo.accessibleAddress')
: t('appOverview.overview.apiInfo.accessibleAddress')}
</div> </div>
<div className="text-sm text-gray-800"> <div className="w-full h-9 pl-2 pr-0.5 py-0.5 bg-black bg-opacity-[0.02] rounded-lg border border-black border-opacity-5 justify-start items-center inline-flex">
{isApp ? appUrl : apiUrl} <div className="h-4 px-2 justify-start items-start gap-2 flex flex-1">
<div className="text-gray-700 text-xs font-medium">
{isApp ? appUrl : apiUrl}
</div>
</div>
<Divider type="vertical" className="!h-3.5 shrink-0 !mx-0.5" />
<CopyFeedback
content={isApp ? appUrl : apiUrl}
selectorId={randomString(8)}
className={'hover:bg-gray-200'}
/>
{/* button copy link/ button regenerate */}
{isApp && (
<Tooltip
content={t('appOverview.overview.appInfo.regenerate') || ''}
selector={`code-generate-${randomString(8)}`}
>
<div
className="w-8 h-8 ml-0.5 cursor-pointer hover:bg-gray-200 rounded-lg"
onClick={onGenCode}
>
<div
className={`w-full h-full ${style.refreshIcon} ${
genLoading ? style.generateLogo : ''
}`}
></div>
</div>
</Tooltip>
)}
</div> </div>
</div> </div>
</div> </div>
<div <div className={'pt-2 flex flex-row items-center'}>
className={`pt-2 flex flex-row items-center ${!isApp ? 'mb-[calc(2rem_+_1px)]' : '' {OPERATIONS_MAP[cardType].map((op: any) => {
}`} const disabled
> = op.opName === t('appOverview.overview.appInfo.settings.entry')
{OPERATIONS_MAP[cardType].map((op) => { ? false
: !runningStatus
return ( return (
<Button <Button
className="mr-2 text-gray-800" className="mr-2 border-[0.5px] hover:outline hover:outline-[0.5px] hover:outline-gray-300 text-gray-700 font-medium bg-white shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]"
key={op.opName} key={op.opName}
onClick={genClickFuncByName(op.opName)} onClick={genClickFuncByName(op.opName)}
disabled={ disabled={disabled}
[t('appOverview.overview.appInfo.preview'), t('appOverview.overview.appInfo.share.entry'), t('appOverview.overview.appInfo.embedded.entry')].includes(op.opName) && !runningStatus
}
> >
<Tooltip <Tooltip
content={t('appOverview.overview.appInfo.preUseReminder') ?? ''} content={
selector={`op-btn-${randomString(16)}`} t('appOverview.overview.appInfo.preUseReminder') ?? ''
className={
([t('appOverview.overview.appInfo.preview'), t('appOverview.overview.appInfo.share.entry'), t('appOverview.overview.appInfo.embedded.entry')].includes(op.opName) && !runningStatus)
? 'mt-[-8px]'
: '!hidden'
} }
selector={`op-btn-${randomString(16)}`}
className={disabled ? 'mt-[-8px]' : '!hidden'}
> >
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<op.opIcon className="h-4 w-4 mr-1.5 stroke-[1.8px]" /> <op.opIcon className="h-4 w-4 mr-1.5 stroke-[1.8px]" />
...@@ -177,30 +232,7 @@ function AppCard({ ...@@ -177,30 +232,7 @@ function AppCard({
</div> </div>
{isApp {isApp
? ( ? (
<div <>
className={
'flex items-center px-6 py-2 box-border text-xs text-gray-500 bg-opacity-50 bg-white border-t-[0.5px] border-primary-50'
}
>
<div
className="flex items-center hover:text-primary-600 hover:cursor-pointer"
onClick={onClickCustomize}
>
<SparklesIcon className="w-4 h-4 mr-1" />
{t('appOverview.overview.appInfo.customize.entry')}
</div>
</div>
)
: null}
{isApp
? (
<div>
<ShareLink
isShow={showShareModal}
onClose={() => setShowShareModal(false)}
linkUrl={appUrl}
onGenerateCode={onGenerateCode}
/>
<SettingsModal <SettingsModal
appInfo={appInfo} appInfo={appInfo}
isShow={showSettingsModal} isShow={showSettingsModal}
...@@ -220,7 +252,7 @@ function AppCard({ ...@@ -220,7 +252,7 @@ function AppCard({
appId={appInfo.id} appId={appInfo.id}
mode={appInfo.mode} mode={appInfo.mode}
/> />
</div> </>
) )
: null} : null}
</div> </div>
......
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.6353 16.5954C21.4501 18.3353 20.4643 19.9658 18.833 20.9076C16.1226 22.4724 12.6569 21.5438 11.0921 18.8335L10.9255 18.5448M10.3641 15.4047C10.5493 13.6647 11.5352 12.0343 13.1665 11.0924C15.8768 9.5276 19.3425 10.4562 20.9073 13.1666L21.074 13.4552M10.3288 20.044L10.8168 18.2227L12.6382 18.7107M19.3616 13.2893L21.183 13.7774L21.671 11.956" stroke="#1D2939" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.6353 16.5954C21.4501 18.3353 20.4643 19.9658 18.833 20.9076C16.1226 22.4724 12.6569 21.5438 11.0921 18.8335L10.9255 18.5448M10.3641 15.4047C10.5493 13.6647 11.5352 12.0343 13.1665 11.0924C15.8768 9.5276 19.3425 10.4562 20.9073 13.1666L21.074 13.4552M10.3288 20.044L10.8168 18.2227L12.6382 18.7107M19.3616 13.2893L21.183 13.7774L21.671 11.956" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import {
ArrowPathIcon,
LinkIcon,
} from '@heroicons/react/24/outline'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
import './style.css'
type IShareLinkProps = {
isShow: boolean
onClose: () => void
onGenerateCode: () => Promise<void>
linkUrl: string
}
const ShareLinkModal: FC<IShareLinkProps> = ({
linkUrl,
isShow,
onClose,
onGenerateCode,
}) => {
const [_, copy] = useCopyToClipboard()
const [genLoading, setGenLoading] = useState(false)
const { t } = useTranslation()
return <Modal
title={t('appOverview.overview.appInfo.share.explanation')}
isShow={isShow}
onClose={onClose}
>
{/* share url */}
<p className='mt-5 text-xs font-medium text-gray-500'>{t('appOverview.overview.appInfo.share.shareUrl')}</p>
{/* input share url */}
<input disabled type='text' value={linkUrl} className='mt-1 w-full bg-gray-50 p-2 text-primary-600 text-xs font-normal outline-gray-50 hover:outline-gray-50 cursor-pointer' />
{/* button copy link/ button regenerate */}
<div className='mt-4 flex gap-3'>
<Button
type="primary"
className='w-32'
onClick={() => {
copy(linkUrl)
}}
>
<LinkIcon className='w-4 h-4 mr-2' />
{t('appOverview.overview.appInfo.share.copyLink')}
</Button>
<Button className='w-32' onClick={async () => {
setGenLoading(true)
await onGenerateCode()
setGenLoading(false)
}}>
<ArrowPathIcon className={`w-4 h-4 mr-2 ${genLoading ? 'generateLogo' : ''}`} />
{t('appOverview.overview.appInfo.share.regenerate')}
</Button>
</div>
</Modal>
}
export default ShareLinkModal
...@@ -16,3 +16,16 @@ ...@@ -16,3 +16,16 @@
@apply w-4 h-4 bg-center bg-no-repeat; @apply w-4 h-4 bg-center bg-no-repeat;
background-image: url(./assets/code-browser.svg); background-image: url(./assets/code-browser.svg);
} }
.refreshIcon {
background-image: url(./assets/refresh.svg);
background-position: center;
background-repeat: no-repeat;
}
.refreshIcon:hover {
background-image: url(./assets/refresh-hover.svg);
background-position: center;
background-repeat: no-repeat;
}
'use client'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { debounce } from 'lodash-es'
import Tooltip from '../tooltip'
import copyStyle from './style.module.css'
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
type Props = {
content: string
selectorId: string
className?: string
}
const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
const CopyFeedback = ({ content, selectorId, className }: Props) => {
const { t } = useTranslation()
const [_, copy] = useCopyToClipboard()
const [isCopied, setIsCopied] = useState<boolean>(false)
const onClickCopy = debounce(() => {
copy(content)
setIsCopied(true)
}, 100)
const onMouseLeave = debounce(() => {
setIsCopied(false)
}, 100)
return (
<Tooltip
selector={`common-copy-feedback-${selectorId}`}
content={
(isCopied
? t(`${prefixEmbedded}.copied`)
: t(`${prefixEmbedded}.copy`)) || ''
}
>
<div
className={`w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg ${
className ?? ''
}`}
onMouseLeave={onMouseLeave}
>
<div
onClick={onClickCopy}
className={`w-full h-full ${copyStyle.copyIcon} ${
isCopied ? copyStyle.copied : ''
}`}
></div>
</div>
</Tooltip>
)
}
export default CopyFeedback
.copyIcon {
background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
background-position: center;
background-repeat: no-repeat;
}
.copyIcon:hover {
background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
background-position: center;
background-repeat: no-repeat;
}
.copyIcon.copied {
background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
}
...@@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from 'react' ...@@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from 'react'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
import { t } from 'i18next' import { t } from 'i18next'
import s from './style.module.css' import s from './style.module.css'
import { randomString } from '@/app/components/app-sidebar/basic' import { randomString } from '@/utils'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
type IInputCopyProps = { type IInputCopyProps = {
......
...@@ -4,7 +4,7 @@ import { t } from 'i18next' ...@@ -4,7 +4,7 @@ import { t } from 'i18next'
import s from './index.module.css' import s from './index.module.css'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import useCopyToClipboard from '@/hooks/use-copy-to-clipboard' import useCopyToClipboard from '@/hooks/use-copy-to-clipboard'
import { randomString } from '@/app/components/app-sidebar/basic' import { randomString } from '@/utils'
type IInvitationLinkProps = { type IInvitationLinkProps = {
value?: string value?: string
......
...@@ -11,13 +11,7 @@ const translation = { ...@@ -11,13 +11,7 @@ const translation = {
explanation: 'Ready-to-use AI WebApp', explanation: 'Ready-to-use AI WebApp',
accessibleAddress: 'Public URL', accessibleAddress: 'Public URL',
preview: 'Preview', preview: 'Preview',
share: { regenerate: 'Regenerate',
entry: 'Share',
explanation: 'Share the following URL to invite more people to access the application.',
shareUrl: 'Share URL',
copyLink: 'Copy Link',
regenerate: 'Regenerate',
},
preUseReminder: 'Please enable WebApp before continuing.', preUseReminder: 'Please enable WebApp before continuing.',
settings: { settings: {
entry: 'Settings', entry: 'Settings',
...@@ -47,7 +41,7 @@ const translation = { ...@@ -47,7 +41,7 @@ const translation = {
}, },
customize: { customize: {
way: 'way', way: 'way',
entry: 'Want to customize your WebApp?', entry: 'Customize',
title: 'Customize AI WebApp', title: 'Customize AI WebApp',
explanation: 'You can customize the frontend of the Web App to fit your scenario and style needs.', explanation: 'You can customize the frontend of the Web App to fit your scenario and style needs.',
way1: { way1: {
......
...@@ -11,13 +11,7 @@ const translation = { ...@@ -11,13 +11,7 @@ const translation = {
explanation: '开箱即用的 AI WebApp', explanation: '开箱即用的 AI WebApp',
accessibleAddress: '公开访问 URL', accessibleAddress: '公开访问 URL',
preview: '预览', preview: '预览',
share: { regenerate: '重新生成',
entry: '分享',
explanation: '将以下网址分享出去,让更多人访问该应用',
shareUrl: '分享 URL',
copyLink: '复制链接',
regenerate: '重新生成',
},
preUseReminder: '使用前请先打开开关', preUseReminder: '使用前请先打开开关',
settings: { settings: {
entry: '设置', entry: '设置',
...@@ -47,7 +41,7 @@ const translation = { ...@@ -47,7 +41,7 @@ const translation = {
}, },
customize: { customize: {
way: '方法', way: '方法',
entry: '想要进一步自定义 WebApp?', entry: '定制化',
title: '定制化 AI WebApp', title: '定制化 AI WebApp',
explanation: '你可以定制化 Web App 前端以符合你的情景与风格需求', explanation: '你可以定制化 Web App 前端以符合你的情景与风格需求',
way1: { way1: {
......
...@@ -14,11 +14,19 @@ export async function asyncRunSafe<T = any>(fn: Promise<T>): Promise<[Error] | [ ...@@ -14,11 +14,19 @@ export async function asyncRunSafe<T = any>(fn: Promise<T>): Promise<[Error] | [
} }
export const getTextWidthWithCanvas = (text: string, font?: string) => { export const getTextWidthWithCanvas = (text: string, font?: string) => {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d')
if (ctx) { if (ctx) {
ctx.font = font ?? '12px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; ctx.font = font ?? '12px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
return Number(ctx.measureText(text).width.toFixed(2)); return Number(ctx.measureText(text).width.toFixed(2))
} }
return 0; return 0
}
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
export function randomString(length: number) {
let result = ''
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]
return result
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment