@lobehub/chat 1.51.2 → 1.51.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/Dockerfile +1 -1
- package/Dockerfile.database +1 -1
- package/changelog/v1.json +12 -0
- package/docs/usage/providers/wenxin.mdx +16 -13
- package/docs/usage/providers/wenxin.zh-CN.mdx +11 -8
- package/package.json +1 -2
- package/src/app/(main)/settings/llm/ProviderList/providers.tsx +2 -4
- package/src/config/aiModels/wenxin.ts +125 -19
- package/src/config/llm.ts +3 -5
- package/src/config/modelProviders/wenxin.ts +100 -23
- package/src/const/auth.ts +0 -3
- package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -3
- package/src/features/Conversation/components/ChatItem/utils.test.ts +284 -0
- package/src/features/Conversation/components/ChatItem/utils.ts +39 -8
- package/src/features/Conversation/components/MarkdownElements/LobeArtifact/rehypePlugin.test.ts +125 -0
- package/src/features/DevPanel/CacheViewer/DataTable/index.tsx +33 -0
- package/src/features/DevPanel/CacheViewer/cacheProvider.tsx +64 -0
- package/src/features/DevPanel/CacheViewer/getCacheEntries.ts +52 -0
- package/src/features/DevPanel/CacheViewer/index.tsx +25 -0
- package/src/features/DevPanel/CacheViewer/schema.ts +49 -0
- package/src/features/DevPanel/FeatureFlagViewer/Form.tsx +93 -0
- package/src/features/DevPanel/FeatureFlagViewer/index.tsx +11 -0
- package/src/features/DevPanel/MetadataViewer/Ld.tsx +25 -0
- package/src/features/DevPanel/MetadataViewer/MetaData.tsx +30 -0
- package/src/features/DevPanel/MetadataViewer/Og.tsx +75 -0
- package/src/features/DevPanel/MetadataViewer/index.tsx +80 -0
- package/src/features/DevPanel/MetadataViewer/useHead.ts +16 -0
- package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +39 -49
- package/src/features/DevPanel/PostgresViewer/{TableColumns.tsx → SchemaSidebar/Columns.tsx} +6 -4
- package/src/features/DevPanel/PostgresViewer/{Schema.tsx → SchemaSidebar/index.tsx} +49 -55
- package/src/features/DevPanel/PostgresViewer/index.tsx +4 -2
- package/src/features/DevPanel/features/FloatPanel.tsx +218 -0
- package/src/features/DevPanel/features/Header.tsx +50 -0
- package/src/features/DevPanel/features/Table/TableCell.tsx +73 -0
- package/src/features/DevPanel/features/Table/TooltipContent.tsx +39 -0
- package/src/features/DevPanel/{PostgresViewer/DataTable/Table.tsx → features/Table/index.tsx} +12 -14
- package/src/features/DevPanel/index.tsx +29 -5
- package/src/libs/agent-runtime/AgentRuntime.test.ts +0 -1
- package/src/libs/agent-runtime/AgentRuntime.ts +7 -0
- package/src/libs/agent-runtime/wenxin/index.ts +10 -107
- package/src/locales/default/modelProvider.ts +0 -20
- package/src/server/modules/AgentRuntime/index.test.ts +0 -21
- package/src/services/_auth.ts +0 -14
- package/src/store/chat/slices/portal/selectors.test.ts +169 -3
- package/src/store/chat/slices/portal/selectors.ts +6 -1
- package/src/store/user/slices/modelList/selectors/keyVaults.ts +0 -2
- package/src/types/aiProvider.ts +0 -1
- package/src/types/user/settings/keyVaults.ts +1 -6
- package/src/app/(backend)/webapi/chat/wenxin/route.test.ts +0 -27
- package/src/app/(backend)/webapi/chat/wenxin/route.ts +0 -30
- package/src/app/(main)/settings/llm/ProviderList/Wenxin/index.tsx +0 -44
- package/src/app/(main)/settings/provider/(detail)/wenxin/page.tsx +0 -61
- package/src/features/Conversation/Error/APIKeyForm/Wenxin.tsx +0 -49
- package/src/features/DevPanel/FloatPanel.tsx +0 -136
- package/src/features/DevPanel/PostgresViewer/DataTable/TableCell.tsx +0 -34
- package/src/libs/agent-runtime/utils/streams/wenxin.test.ts +0 -153
- package/src/libs/agent-runtime/utils/streams/wenxin.ts +0 -38
- package/src/libs/agent-runtime/wenxin/type.ts +0 -84
package/src/features/Conversation/components/MarkdownElements/LobeArtifact/rehypePlugin.test.ts
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
2
|
+
|
3
|
+
import rehypePlugin from './rehypePlugin';
|
4
|
+
|
5
|
+
describe('rehypePlugin', () => {
|
6
|
+
it('should transform <lobeArtifact> tags with attributes', () => {
|
7
|
+
const tree = {
|
8
|
+
type: 'root',
|
9
|
+
children: [
|
10
|
+
{
|
11
|
+
type: 'element',
|
12
|
+
tagName: 'p',
|
13
|
+
children: [
|
14
|
+
{
|
15
|
+
type: 'raw',
|
16
|
+
value: '<lobeArtifact identifier="test-id" type="image/svg+xml" title="Test Title">',
|
17
|
+
},
|
18
|
+
{ type: 'text', value: 'Artifact content' },
|
19
|
+
{ type: 'raw', value: '</lobeArtifact>' },
|
20
|
+
],
|
21
|
+
},
|
22
|
+
],
|
23
|
+
};
|
24
|
+
|
25
|
+
const expectedTree = {
|
26
|
+
type: 'root',
|
27
|
+
children: [
|
28
|
+
{
|
29
|
+
type: 'element',
|
30
|
+
tagName: 'lobeArtifact',
|
31
|
+
properties: {
|
32
|
+
identifier: 'test-id',
|
33
|
+
type: 'image/svg+xml',
|
34
|
+
title: 'Test Title',
|
35
|
+
},
|
36
|
+
children: [{ type: 'text', value: 'Artifact content' }],
|
37
|
+
},
|
38
|
+
],
|
39
|
+
};
|
40
|
+
|
41
|
+
const plugin = rehypePlugin();
|
42
|
+
plugin(tree);
|
43
|
+
|
44
|
+
expect(tree).toEqual(expectedTree);
|
45
|
+
});
|
46
|
+
|
47
|
+
it('should handle mixed content with thinking tags and plain text', () => {
|
48
|
+
const tree = {
|
49
|
+
type: 'root',
|
50
|
+
children: [
|
51
|
+
{
|
52
|
+
type: 'element',
|
53
|
+
tagName: 'p',
|
54
|
+
children: [{ type: 'text', value: 'Initial plain text paragraph' }],
|
55
|
+
},
|
56
|
+
{
|
57
|
+
type: 'element',
|
58
|
+
tagName: 'p',
|
59
|
+
children: [
|
60
|
+
{ type: 'raw', value: '<lobeThinking>' },
|
61
|
+
{ type: 'text', value: 'AI is thinking...' },
|
62
|
+
{ type: 'raw', value: '</lobeThinking>' },
|
63
|
+
],
|
64
|
+
},
|
65
|
+
{
|
66
|
+
type: 'element',
|
67
|
+
tagName: 'p',
|
68
|
+
children: [
|
69
|
+
{
|
70
|
+
type: 'raw',
|
71
|
+
value: '<lobeArtifact identifier="test-id" type="image/svg+xml" title="Test Title">',
|
72
|
+
},
|
73
|
+
{ type: 'text', value: 'Artifact content' },
|
74
|
+
{ type: 'raw', value: '</lobeArtifact>' },
|
75
|
+
],
|
76
|
+
},
|
77
|
+
{
|
78
|
+
type: 'element',
|
79
|
+
tagName: 'p',
|
80
|
+
children: [{ type: 'text', value: 'Final plain text paragraph' }],
|
81
|
+
},
|
82
|
+
],
|
83
|
+
};
|
84
|
+
|
85
|
+
const expectedTree = {
|
86
|
+
type: 'root',
|
87
|
+
children: [
|
88
|
+
{
|
89
|
+
type: 'element',
|
90
|
+
tagName: 'p',
|
91
|
+
children: [{ type: 'text', value: 'Initial plain text paragraph' }],
|
92
|
+
},
|
93
|
+
{
|
94
|
+
type: 'element',
|
95
|
+
tagName: 'p',
|
96
|
+
children: [
|
97
|
+
{ type: 'raw', value: '<lobeThinking>' },
|
98
|
+
{ type: 'text', value: 'AI is thinking...' },
|
99
|
+
{ type: 'raw', value: '</lobeThinking>' },
|
100
|
+
],
|
101
|
+
},
|
102
|
+
{
|
103
|
+
type: 'element',
|
104
|
+
tagName: 'lobeArtifact',
|
105
|
+
properties: {
|
106
|
+
identifier: 'test-id',
|
107
|
+
type: 'image/svg+xml',
|
108
|
+
title: 'Test Title',
|
109
|
+
},
|
110
|
+
children: [{ type: 'text', value: 'Artifact content' }],
|
111
|
+
},
|
112
|
+
{
|
113
|
+
type: 'element',
|
114
|
+
tagName: 'p',
|
115
|
+
children: [{ type: 'text', value: 'Final plain text paragraph' }],
|
116
|
+
},
|
117
|
+
],
|
118
|
+
};
|
119
|
+
|
120
|
+
const plugin = rehypePlugin();
|
121
|
+
plugin(tree);
|
122
|
+
|
123
|
+
expect(tree).toEqual(expectedTree);
|
124
|
+
});
|
125
|
+
});
|
@@ -0,0 +1,33 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { RefreshCw } from 'lucide-react';
|
4
|
+
import { memo } from 'react';
|
5
|
+
|
6
|
+
import Header from '../../features/Header';
|
7
|
+
import Table from '../../features/Table';
|
8
|
+
import { useCachePanelContext } from '../cacheProvider';
|
9
|
+
|
10
|
+
const DataTable = memo(() => {
|
11
|
+
const { entries, isLoading, refreshData } = useCachePanelContext();
|
12
|
+
return (
|
13
|
+
<>
|
14
|
+
<Header
|
15
|
+
actions={[
|
16
|
+
{
|
17
|
+
icon: RefreshCw,
|
18
|
+
onClick: () => refreshData(),
|
19
|
+
title: 'Refresh',
|
20
|
+
},
|
21
|
+
]}
|
22
|
+
title="Cache Entries"
|
23
|
+
/>
|
24
|
+
<Table
|
25
|
+
columns={['url', 'headers.content-type', 'body', 'kind', 'tags', 'revalidate', 'timestamp']}
|
26
|
+
dataSource={entries}
|
27
|
+
loading={isLoading}
|
28
|
+
/>
|
29
|
+
</>
|
30
|
+
);
|
31
|
+
});
|
32
|
+
|
33
|
+
export default DataTable;
|
@@ -0,0 +1,64 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { usePathname } from 'next/navigation';
|
4
|
+
import {
|
5
|
+
PropsWithChildren,
|
6
|
+
createContext,
|
7
|
+
useContext,
|
8
|
+
useEffect,
|
9
|
+
useState,
|
10
|
+
useTransition,
|
11
|
+
} from 'react';
|
12
|
+
|
13
|
+
import { getCacheFiles } from './getCacheEntries';
|
14
|
+
import type { NextCacheFileData } from './schema';
|
15
|
+
|
16
|
+
interface CachePanelContextProps {
|
17
|
+
entries: NextCacheFileData[];
|
18
|
+
isLoading: boolean;
|
19
|
+
refreshData: () => void;
|
20
|
+
setEntries: (value: NextCacheFileData[]) => void;
|
21
|
+
}
|
22
|
+
|
23
|
+
const CachePanelContext = createContext<CachePanelContextProps>({
|
24
|
+
entries: [],
|
25
|
+
isLoading: false,
|
26
|
+
refreshData: () => {},
|
27
|
+
setEntries: () => {},
|
28
|
+
});
|
29
|
+
|
30
|
+
export const useCachePanelContext = () => useContext(CachePanelContext);
|
31
|
+
|
32
|
+
export const CachePanelContextProvider = (
|
33
|
+
props: PropsWithChildren<{
|
34
|
+
entries: NextCacheFileData[];
|
35
|
+
}>,
|
36
|
+
) => {
|
37
|
+
const [isLoading, startTransition] = useTransition();
|
38
|
+
const [entries, setEntries] = useState(props.entries);
|
39
|
+
const pathname = usePathname();
|
40
|
+
|
41
|
+
const refreshData = () => {
|
42
|
+
startTransition(async () => {
|
43
|
+
const files = await getCacheFiles();
|
44
|
+
setEntries(files ?? []);
|
45
|
+
});
|
46
|
+
};
|
47
|
+
|
48
|
+
useEffect(() => {
|
49
|
+
refreshData();
|
50
|
+
}, [pathname]);
|
51
|
+
|
52
|
+
return (
|
53
|
+
<CachePanelContext.Provider
|
54
|
+
value={{
|
55
|
+
entries,
|
56
|
+
isLoading,
|
57
|
+
refreshData,
|
58
|
+
setEntries,
|
59
|
+
}}
|
60
|
+
>
|
61
|
+
{props.children}
|
62
|
+
</CachePanelContext.Provider>
|
63
|
+
);
|
64
|
+
};
|
@@ -0,0 +1,52 @@
|
|
1
|
+
'use server';
|
2
|
+
|
3
|
+
import { existsSync, promises } from 'node:fs';
|
4
|
+
import pMap from 'p-map';
|
5
|
+
import { ZodError } from 'zod';
|
6
|
+
|
7
|
+
import { type NextCacheFileData, nextCacheFileSchema } from './schema';
|
8
|
+
|
9
|
+
const cachePath = '.next/cache/fetch-cache';
|
10
|
+
|
11
|
+
export const getCacheFiles = async (): Promise<NextCacheFileData[]> => {
|
12
|
+
if (!existsSync(cachePath)) {
|
13
|
+
return [];
|
14
|
+
}
|
15
|
+
const files = await promises.readdir(cachePath);
|
16
|
+
let result: NextCacheFileData[] = (await pMap(files, async (file) => {
|
17
|
+
// ignore tags-manifest file
|
18
|
+
if (/manifest/.test(file)) return false;
|
19
|
+
try {
|
20
|
+
const fileContent = await promises.readFile(`${cachePath}/${file}`).catch((err) => {
|
21
|
+
throw new Error(`Error reading file ${file}`, {
|
22
|
+
cause: err,
|
23
|
+
});
|
24
|
+
});
|
25
|
+
|
26
|
+
const fileStats = await promises.stat(`${cachePath}/${file}`).catch((err) => {
|
27
|
+
throw new Error(`Error reading file ${file}`, {
|
28
|
+
cause: err,
|
29
|
+
});
|
30
|
+
});
|
31
|
+
|
32
|
+
const jsonData = JSON.parse(fileContent.toString());
|
33
|
+
|
34
|
+
return nextCacheFileSchema.parse({
|
35
|
+
...jsonData,
|
36
|
+
id: file,
|
37
|
+
timestamp: new Date(fileStats.birthtime),
|
38
|
+
});
|
39
|
+
} catch (error) {
|
40
|
+
if (error instanceof ZodError) {
|
41
|
+
const issues = error.issues;
|
42
|
+
console.error(`File ${file} do not match the schema`, issues);
|
43
|
+
}
|
44
|
+
console.error(`Error parsing ${file}`);
|
45
|
+
return false;
|
46
|
+
}
|
47
|
+
})) as NextCacheFileData[];
|
48
|
+
|
49
|
+
result = result.filter(Boolean) as NextCacheFileData[];
|
50
|
+
|
51
|
+
return result.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
52
|
+
};
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { Empty } from 'antd';
|
2
|
+
import { Center } from 'react-layout-kit';
|
3
|
+
|
4
|
+
import DataTable from './DataTable';
|
5
|
+
import { CachePanelContextProvider } from './cacheProvider';
|
6
|
+
import { getCacheFiles } from './getCacheEntries';
|
7
|
+
|
8
|
+
const CacheViewer = async () => {
|
9
|
+
const files = await getCacheFiles();
|
10
|
+
|
11
|
+
if (!files || files.length === 0)
|
12
|
+
return (
|
13
|
+
<Center height={'80%'}>
|
14
|
+
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
15
|
+
</Center>
|
16
|
+
);
|
17
|
+
|
18
|
+
return (
|
19
|
+
<CachePanelContextProvider entries={files}>
|
20
|
+
<DataTable />
|
21
|
+
</CachePanelContextProvider>
|
22
|
+
);
|
23
|
+
};
|
24
|
+
|
25
|
+
export default CacheViewer;
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import { z } from 'zod';
|
2
|
+
|
3
|
+
const unstableCacheFileSchema = z.object({
|
4
|
+
data: z.object({
|
5
|
+
body: z.string(),
|
6
|
+
headers: z.object({}).transform(() => null),
|
7
|
+
status: z.number(),
|
8
|
+
url: z.literal(''),
|
9
|
+
}),
|
10
|
+
kind: z.union([z.literal('FETCH'), z.unknown()]),
|
11
|
+
revalidate: z.number().optional(),
|
12
|
+
tags: z.array(z.string()).optional().default([]),
|
13
|
+
});
|
14
|
+
|
15
|
+
const fetchCacheFileSchema = z.object({
|
16
|
+
data: z.object({
|
17
|
+
body: z.string(),
|
18
|
+
headers: z.record(z.string(), z.string()),
|
19
|
+
status: z.number(),
|
20
|
+
url: z.string().url(),
|
21
|
+
}),
|
22
|
+
id: z.string(),
|
23
|
+
kind: z.union([z.literal('FETCH'), z.unknown()]),
|
24
|
+
revalidate: z.number().optional(),
|
25
|
+
tags: z.array(z.string()).optional().default([]),
|
26
|
+
});
|
27
|
+
|
28
|
+
const atou = (str: string, type: string) => {
|
29
|
+
if (type.startsWith('image/')) return `data:${type};base64,${str}`;
|
30
|
+
return Buffer.from(str, 'base64').toString();
|
31
|
+
};
|
32
|
+
export const nextCacheFileSchema = z
|
33
|
+
.union([unstableCacheFileSchema, fetchCacheFileSchema])
|
34
|
+
.transform((item) => {
|
35
|
+
const { data, ...cacheEntry } = item;
|
36
|
+
const body =
|
37
|
+
data.url !== ''
|
38
|
+
? atou(data.body, data.headers ? data.headers['content-type'] : '')
|
39
|
+
: data.body;
|
40
|
+
return {
|
41
|
+
...cacheEntry,
|
42
|
+
...data,
|
43
|
+
body,
|
44
|
+
timestamp: data.headers?.date ? new Date(data.headers?.date) : new Date(),
|
45
|
+
url: data.url === '' ? 'unstable_cache' : data.url,
|
46
|
+
};
|
47
|
+
});
|
48
|
+
|
49
|
+
export type NextCacheFileData = z.infer<typeof nextCacheFileSchema>;
|
@@ -0,0 +1,93 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { Form, Highlighter } from '@lobehub/ui';
|
4
|
+
import { Switch } from 'antd';
|
5
|
+
import { createStyles } from 'antd-style';
|
6
|
+
import { snakeCase } from 'lodash-es';
|
7
|
+
import { ListRestartIcon } from 'lucide-react';
|
8
|
+
import { memo, useMemo, useState } from 'react';
|
9
|
+
import { Flexbox } from 'react-layout-kit';
|
10
|
+
|
11
|
+
import { DEFAULT_FEATURE_FLAGS } from '@/config/featureFlags';
|
12
|
+
|
13
|
+
import Header from '../features/Header';
|
14
|
+
|
15
|
+
const useStyles = createStyles(({ css, token, prefixCls }) => ({
|
16
|
+
container: css`
|
17
|
+
* {
|
18
|
+
font-family: ${token.fontFamilyCode};
|
19
|
+
font-size: 12px;
|
20
|
+
}
|
21
|
+
.${prefixCls}-form-item {
|
22
|
+
padding-block: 4px !important;
|
23
|
+
}
|
24
|
+
`,
|
25
|
+
}));
|
26
|
+
|
27
|
+
const FeatureFlagForm = memo<{ flags: any }>(({ flags }) => {
|
28
|
+
const { styles } = useStyles();
|
29
|
+
const [data, setData] = useState(flags);
|
30
|
+
const [form] = Form.useForm();
|
31
|
+
|
32
|
+
const output = useMemo(
|
33
|
+
() =>
|
34
|
+
Object.entries(data).map(([key, value]) => {
|
35
|
+
const flag = snakeCase(key);
|
36
|
+
// @ts-ignore
|
37
|
+
if (DEFAULT_FEATURE_FLAGS[flag] === value) return false;
|
38
|
+
if (value === true) return `+${flag}`;
|
39
|
+
return `-${flag}`;
|
40
|
+
}),
|
41
|
+
[data],
|
42
|
+
);
|
43
|
+
|
44
|
+
return (
|
45
|
+
<>
|
46
|
+
<Header
|
47
|
+
actions={[
|
48
|
+
{
|
49
|
+
icon: ListRestartIcon,
|
50
|
+
onClick: () => {
|
51
|
+
form.resetFields();
|
52
|
+
setData(flags);
|
53
|
+
},
|
54
|
+
title: 'Reset',
|
55
|
+
},
|
56
|
+
]}
|
57
|
+
title={'Feature Flag Env'}
|
58
|
+
/>
|
59
|
+
<Flexbox
|
60
|
+
className={styles.container}
|
61
|
+
height={'100%'}
|
62
|
+
paddingInline={16}
|
63
|
+
style={{ overflow: 'auto', position: 'relative' }}
|
64
|
+
width={'100%'}
|
65
|
+
>
|
66
|
+
<Form
|
67
|
+
form={form}
|
68
|
+
initialValues={flags}
|
69
|
+
itemMinWidth={'max(75%,240px)'}
|
70
|
+
items={Object.keys(flags).map((key) => {
|
71
|
+
return {
|
72
|
+
children: <Switch size={'small'} />,
|
73
|
+
label: snakeCase(key),
|
74
|
+
minWidth: undefined,
|
75
|
+
name: key,
|
76
|
+
valuePropName: 'checked',
|
77
|
+
};
|
78
|
+
})}
|
79
|
+
itemsType={'flat'}
|
80
|
+
onValuesChange={(_, v) => setData(v)}
|
81
|
+
variant={'pure'}
|
82
|
+
/>
|
83
|
+
</Flexbox>
|
84
|
+
<Highlighter
|
85
|
+
language={'env'}
|
86
|
+
style={{ flex: 'none', fontSize: 12 }}
|
87
|
+
wrap
|
88
|
+
>{`FEATURE_FLAGS="${output.filter(Boolean).join(',')}"`}</Highlighter>
|
89
|
+
</>
|
90
|
+
);
|
91
|
+
});
|
92
|
+
|
93
|
+
export default FeatureFlagForm;
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { getServerFeatureFlagsValue } from '@/config/featureFlags';
|
2
|
+
|
3
|
+
import FeatureFlagForm from './Form';
|
4
|
+
|
5
|
+
const FeatureFlagViewer = () => {
|
6
|
+
const serverFeatureFlags = getServerFeatureFlagsValue();
|
7
|
+
|
8
|
+
return <FeatureFlagForm flags={serverFeatureFlags} />;
|
9
|
+
};
|
10
|
+
|
11
|
+
export default FeatureFlagViewer;
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { Highlighter } from '@lobehub/ui';
|
2
|
+
import { Empty } from 'antd';
|
3
|
+
import { memo } from 'react';
|
4
|
+
import { Center } from 'react-layout-kit';
|
5
|
+
|
6
|
+
import { useLd } from './useHead';
|
7
|
+
|
8
|
+
const Ld = memo(() => {
|
9
|
+
const ld = useLd();
|
10
|
+
|
11
|
+
if (!ld)
|
12
|
+
return (
|
13
|
+
<Center height={'80%'}>
|
14
|
+
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
15
|
+
</Center>
|
16
|
+
);
|
17
|
+
|
18
|
+
return (
|
19
|
+
<Highlighter language="json" type={'pure'}>
|
20
|
+
{JSON.stringify(JSON.parse(ld), null, 2)}
|
21
|
+
</Highlighter>
|
22
|
+
);
|
23
|
+
});
|
24
|
+
|
25
|
+
export default Ld;
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import { Form } from '@lobehub/ui';
|
2
|
+
import { Input } from 'antd';
|
3
|
+
import { memo } from 'react';
|
4
|
+
|
5
|
+
import { useHead, useTitle } from './useHead';
|
6
|
+
|
7
|
+
const MetaData = memo(() => {
|
8
|
+
const title = useTitle();
|
9
|
+
const description = useHead('name', 'description');
|
10
|
+
|
11
|
+
return (
|
12
|
+
<Form
|
13
|
+
itemMinWidth={'max(75%,240px)'}
|
14
|
+
items={[
|
15
|
+
{
|
16
|
+
children: <Input value={title} variant={'filled'} />,
|
17
|
+
label: `Title (${title.length})`,
|
18
|
+
},
|
19
|
+
{
|
20
|
+
children: <Input.TextArea rows={2} value={description} variant={'filled'} />,
|
21
|
+
label: `Description (${description.length})`,
|
22
|
+
},
|
23
|
+
]}
|
24
|
+
itemsType={'flat'}
|
25
|
+
variant={'pure'}
|
26
|
+
/>
|
27
|
+
);
|
28
|
+
});
|
29
|
+
|
30
|
+
export default MetaData;
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import { Form } from '@lobehub/ui';
|
2
|
+
import { Input } from 'antd';
|
3
|
+
import Image from 'next/image';
|
4
|
+
import { memo } from 'react';
|
5
|
+
import { Flexbox } from 'react-layout-kit';
|
6
|
+
|
7
|
+
import { useHead } from './useHead';
|
8
|
+
|
9
|
+
const MetaData = memo(() => {
|
10
|
+
const ogTitle = useHead('property', 'og:title');
|
11
|
+
const ogDescription = useHead('property', 'og:description');
|
12
|
+
const ogImage = useHead('property', 'og:image');
|
13
|
+
|
14
|
+
return (
|
15
|
+
<Form
|
16
|
+
itemMinWidth={'max(75%,240px)'}
|
17
|
+
items={[
|
18
|
+
{
|
19
|
+
children: <Input value={ogTitle} variant={'filled'} />,
|
20
|
+
label: `OG Title (${ogTitle.length})`,
|
21
|
+
},
|
22
|
+
{
|
23
|
+
children: <Input.TextArea rows={2} value={ogDescription} variant={'filled'} />,
|
24
|
+
label: `OG Description (${ogDescription.length})`,
|
25
|
+
},
|
26
|
+
{
|
27
|
+
children: (
|
28
|
+
<Flexbox
|
29
|
+
height={186}
|
30
|
+
style={{
|
31
|
+
background: 'rgba(0, 0, 0, .5)',
|
32
|
+
borderRadius: 14,
|
33
|
+
overflow: 'hidden',
|
34
|
+
position: 'relative',
|
35
|
+
}}
|
36
|
+
width={358}
|
37
|
+
>
|
38
|
+
<div
|
39
|
+
style={{
|
40
|
+
background: 'rgba(0, 0, 0, .5)',
|
41
|
+
borderRadius: 4,
|
42
|
+
bottom: 10,
|
43
|
+
left: 10,
|
44
|
+
lineHeight: 1.3,
|
45
|
+
padding: '2px 6px',
|
46
|
+
position: 'absolute',
|
47
|
+
zIndex: 10,
|
48
|
+
}}
|
49
|
+
>
|
50
|
+
lobehub.com
|
51
|
+
</div>
|
52
|
+
<Image
|
53
|
+
alt={'og'}
|
54
|
+
fill
|
55
|
+
src={ogImage}
|
56
|
+
style={{ objectFit: 'cover' }}
|
57
|
+
unoptimized={true}
|
58
|
+
/>
|
59
|
+
</Flexbox>
|
60
|
+
),
|
61
|
+
label: 'Og Image',
|
62
|
+
minWidth: undefined,
|
63
|
+
},
|
64
|
+
{
|
65
|
+
children: <Input value={ogImage} variant={'filled'} />,
|
66
|
+
label: 'Og Image Url',
|
67
|
+
},
|
68
|
+
]}
|
69
|
+
itemsType={'flat'}
|
70
|
+
variant={'pure'}
|
71
|
+
/>
|
72
|
+
);
|
73
|
+
});
|
74
|
+
|
75
|
+
export default MetaData;
|
@@ -0,0 +1,80 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { TabsNav } from '@lobehub/ui';
|
4
|
+
import { createStyles } from 'antd-style';
|
5
|
+
import { memo, useState } from 'react';
|
6
|
+
import { Flexbox } from 'react-layout-kit';
|
7
|
+
|
8
|
+
import Header from '../features/Header';
|
9
|
+
import Ld from './Ld';
|
10
|
+
import MetaData from './MetaData';
|
11
|
+
import Og from './Og';
|
12
|
+
|
13
|
+
const useStyles = createStyles(({ css, prefixCls }) => ({
|
14
|
+
container: css`
|
15
|
+
* {
|
16
|
+
font-size: 12px;
|
17
|
+
}
|
18
|
+
.${prefixCls}-form-item {
|
19
|
+
padding-block: 8px;
|
20
|
+
}
|
21
|
+
`,
|
22
|
+
}));
|
23
|
+
|
24
|
+
enum Tab {
|
25
|
+
Ld = 'ld',
|
26
|
+
Meta = 'meta',
|
27
|
+
Og = 'og',
|
28
|
+
}
|
29
|
+
|
30
|
+
const MetadataViewer = memo(() => {
|
31
|
+
const { styles } = useStyles();
|
32
|
+
const [active, setActive] = useState<Tab>(Tab.Og);
|
33
|
+
return (
|
34
|
+
<Flexbox
|
35
|
+
className={styles.container}
|
36
|
+
height={'100%'}
|
37
|
+
style={{ overflow: 'hidden', position: 'relative' }}
|
38
|
+
width={'100%'}
|
39
|
+
>
|
40
|
+
<Header
|
41
|
+
style={{ paddingInlineStart: 0 }}
|
42
|
+
title={
|
43
|
+
<TabsNav
|
44
|
+
activeKey={active}
|
45
|
+
items={[
|
46
|
+
{
|
47
|
+
key: Tab.Og,
|
48
|
+
label: 'OG',
|
49
|
+
},
|
50
|
+
{
|
51
|
+
key: Tab.Meta,
|
52
|
+
label: 'MetaData',
|
53
|
+
},
|
54
|
+
{
|
55
|
+
key: Tab.Ld,
|
56
|
+
label: 'StructuredData',
|
57
|
+
},
|
58
|
+
]}
|
59
|
+
onChange={(v) => setActive(v as Tab)}
|
60
|
+
style={{ margin: 16 }}
|
61
|
+
variant={'compact'}
|
62
|
+
/>
|
63
|
+
}
|
64
|
+
/>
|
65
|
+
<Flexbox
|
66
|
+
flex={1}
|
67
|
+
height={'100%'}
|
68
|
+
paddingInline={16}
|
69
|
+
style={{ overflow: 'auto', paddingBottom: 16, position: 'relative' }}
|
70
|
+
width={'100%'}
|
71
|
+
>
|
72
|
+
{active === Tab.Og && <Og />}
|
73
|
+
{active === Tab.Meta && <MetaData />}
|
74
|
+
{active === Tab.Ld && <Ld />}
|
75
|
+
</Flexbox>
|
76
|
+
</Flexbox>
|
77
|
+
);
|
78
|
+
});
|
79
|
+
|
80
|
+
export default MetadataViewer;
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import { isOnServerSide } from '@/utils/env';
|
2
|
+
|
3
|
+
export const useHead = (prop: string, name: string) => {
|
4
|
+
if (isOnServerSide) return '';
|
5
|
+
return document.querySelector(`meta[${prop}='${name}']`)?.getAttribute('content') || '';
|
6
|
+
};
|
7
|
+
|
8
|
+
export const useTitle = () => {
|
9
|
+
if (isOnServerSide) return '';
|
10
|
+
return document.querySelector(`title`)?.innerHTML || '';
|
11
|
+
};
|
12
|
+
|
13
|
+
export const useLd = () => {
|
14
|
+
if (isOnServerSide) return '';
|
15
|
+
return document.querySelector(`script[type='application/ld+json']`)?.innerHTML || '';
|
16
|
+
};
|