feat: Add agent share

This commit is contained in:
canisminor1990 2023-10-19 01:28:23 +08:00
parent c45526a811
commit 953d7c73b3
34 changed files with 534 additions and 105 deletions

View file

@ -1,23 +1,23 @@
{
"agentDefaultMessage": "Hello, I'm **{{name}}**. You can start chatting with me right away or go to [Assistant Settings](/chat/settings#session={{id}}) to improve my information.",
"agentDefaultMessage": "Hello, I'm **{{name}}**. You can start chatting with me right away or go to [Agent Settings](/chat/settings#session={{id}}) to improve my information.",
"agentDefaultMessageWithSystemRole": "Hello, I'm **{{name}}**, {{systemRole}}. Let's start the conversation!",
"backToBottom": "Go to Latest Messages",
"clearCurrentMessages": "Clear Current Session Messages",
"confirmClearCurrentMessages": "You are about to clear the current session messages. Once cleared, they cannot be recovered. Please confirm your operation.",
"confirmRemoveSessionItemAlert": "You are about to delete this assistant. Once deleted, it cannot be recovered. Please confirm your operation.",
"defaultAgent": "Custom Assistant",
"defaultSession": "Custom Assistant",
"confirmRemoveSessionItemAlert": "You are about to delete this agent. Once deleted, it cannot be recovered. Please confirm your operation.",
"defaultAgent": "Custom Agent",
"defaultSession": "Custom Agent",
"historyRange": "History Range",
"inbox": {
"defaultMessage": "Hello, I'm your intelligent assistant. You can ask me any questions, and I will do my best to answer you. If you need a more professional or customized assistant, you can click on `+` to create a custom assistant.",
"desc": "Activate brain clusters and spark thinking. Your intelligent assistant is here to communicate with you about everything.",
"defaultMessage": "Hello, I'm your intelligent agent. You can ask me any questions, and I will do my best to answer you. If you need a more professional or customized agent, you can click on `+` to create a custom agent.",
"desc": "Activate brain clusters and spark thinking. Your intelligent agent is here to communicate with you about everything.",
"title": "Chat Randomly"
},
"newAgent": "Create New Assistant",
"newAgent": "Create New Agent",
"noDescription": "No description available",
"regenerate": "Regenerate",
"roleAndArchive": "Roles and Archives",
"searchAgentPlaceholder": "Search assistants and conversations...",
"searchAgentPlaceholder": "Search agents and conversations...",
"send": "Send",
"sendPlaceholder": "Enter chat content...",
"shareModal": {
@ -29,7 +29,7 @@
"withBackground": "Include Background Image",
"withFooter": "Include Footer",
"withPluginInfo": "Include Plugin Information",
"withSystemRole": "Include Assistant Role Setting"
"withSystemRole": "Include Agent Role Setting"
},
"stop": "Stop",
"temp": "Temporary",
@ -50,6 +50,6 @@
"clear": "Clear Translation"
},
"translateTo": "Translate",
"updateAgent": "Update Assistant Information",
"updateAgent": "Update Agent Information",
"warp": "Line Break"
}

View file

@ -235,6 +235,13 @@
},
"title": "Theme Settings"
},
"submitAgentModal": {
"tooltips": "Share to Assistant Market",
"button": "Submit Assistant",
"identifier": "Identifier",
"metaMiss": "Please complete the assistant information before submitting. It should include name, description, and tags.",
"placeholder": "Please enter a unique identifier for the assistant, such as web-development."
},
"tab": {
"agent": "Default Agent",
"common": "Common Settings",

View file

@ -222,6 +222,13 @@
},
"title": "テーマ設定"
},
"submitAgentModal": {
"tooltips": "アシスタントマーケットに共有する",
"button": "助手を提出する",
"identifier": "識別子 エージェントの識別子",
"metaMiss": "エージェント情報を入力してから提出してください。名前、説明、およびタグが必要です",
"placeholder": "エージェントの識別子を入力してください。一意である必要があります。例web-development"
},
"tab": {
"agent": "デフォルトのアシスタント",
"common": "一般設定",

View file

@ -222,6 +222,13 @@
},
"title": "테마 설정"
},
"submitAgentModal": {
"tooltips": "도우미 마켓에 공유",
"button": "도우미 제출",
"identifier": "식별자 도우미 식별자",
"metaMiss": "도우미 정보를 입력한 후 제출하세요. 이름, 설명 및 태그를 포함해야 합니다.",
"placeholder": "도우미의 식별자를 입력하세요. 고유해야 하며, 예를 들어 web-development과 같은 형식이어야 합니다."
},
"tab": {
"agent": "기본 도우미",
"common": "일반 설정",

View file

@ -1,7 +1,8 @@
{
"about": "Наш Github",
"advanceSettings": "Дополнительные настройки",
"agentDefaultMessage": "Здравствуйте, я **{{name}}**. Вы можете начать общаться со мной прямо сейчас или перейти на [Assistant Settings](/chat/settings#session={{id}}) для моей настройки.",
"agentDefaultMessage": "Здравствуйте, я **{{name}}**. Вы можете начать общаться со мной прямо сейчас или перейти на [Agent Settings](/chat/settings#session={{id}}) для моей настройки.",
"agentMaxToken": "Максимальная длина сессии",
"agentModel": "Модель",
"agentProfile": "Информация о помощнике",
"appInitializing": "LobeChat запускается, пожалуйста, подождите...",

View file

@ -235,6 +235,13 @@
},
"title": "Настройки темы"
},
"submitAgentModal": {
"tooltips": "Поделиться на рынке помощников",
"button": "Отправить агента",
"identifier": "Идентификатор агента",
"metaMiss": "Пожалуйста, заполните информацию об агенте перед отправкой. Необходимо указать имя, описание и метки",
"placeholder": "Введите идентификатор агента, который должен быть уникальным, например, web-development"
},
"tab": {
"agent": "Помощник по умолчанию",
"common": "Общие настройки",

View file

@ -222,6 +222,13 @@
},
"title": "主题设置"
},
"submitAgentModal": {
"button": "提交助手",
"identifier": "identifier 助手标识符",
"metaMiss": "请补全助手信息后提交,需要包含名称、描述和标签",
"placeholder": "请输入助手的标识符,需要是唯一的,比如 web-development",
"tooltips": "分享到助手市场"
},
"tab": {
"agent": "默认助手",
"common": "通用设置",

View file

@ -235,6 +235,13 @@
},
"title": "主題設定"
},
"submitAgentModal": {
"tooltips": "分享到助手市場",
"button": "提交助手",
"identifier": "助手識別符",
"metaMiss": "請補全助手資訊後提交,需要包含名稱、描述和標籤",
"placeholder": "請輸入助手的識別符,需要是唯一的,例如 web-development"
},
"tab": {
"agent": "預設助理",
"common": "通用設定",

View file

@ -69,7 +69,7 @@
"@emoji-mart/data": "^1",
"@emoji-mart/react": "^1",
"@icons-pack/react-simple-icons": "^9",
"@lobehub/chat-plugin-sdk": "^1.17.8",
"@lobehub/chat-plugin-sdk": "latest",
"@lobehub/chat-plugins-gateway": "latest",
"@lobehub/ui": "latest",
"@vercel/analytics": "^1",
@ -79,7 +79,7 @@
"antd-style": "^3.5",
"brotli-wasm": "^1",
"chroma-js": "^2",
"copy-to-clipboard": "^3.3.3",
"copy-to-clipboard": "^3",
"dayjs": "^1",
"emoji-mart": "^5",
"fast-deep-equal": "^3",
@ -98,6 +98,7 @@
"openai": "^4.10.0",
"polished": "^4",
"posthog-js": "^1",
"query-string": "^8",
"react": "^18",
"react-dom": "^18",
"react-hotkeys-hook": "^4",
@ -146,7 +147,7 @@
"eslint": "^8",
"husky": "^8",
"jsdom": "^22",
"lint-staged": "^15.0.0",
"lint-staged": "^15",
"lodash": "^4",
"next-pwa": "^5",
"node-fetch": "^3",

View file

@ -13,9 +13,12 @@ export const readJSON = (filePath: string) => {
return JSON.parse(data);
};
export const replaceAssistantToAgent = (text: string) =>
text.replaceAll('assistant', 'agent').replaceAll('Assistant', 'Agent');
export const writeJSON = (filePath: string, data: any) => {
const jsonStr = JSON.stringify(data, null, 2);
writeFileSync(filePath, jsonStr, 'utf8');
writeFileSync(filePath, replaceAssistantToAgent(jsonStr), 'utf8');
};
export const genResourcesContent = (locales: string[]) => {

View file

@ -5,6 +5,7 @@ import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import AgentInfo from '@/features/AgentInfo';
import { useSessionChatInit, useSessionStore } from '@/store/session';
import { agentSelectors } from '@/store/session/selectors';
@ -15,8 +16,9 @@ const SystemRole = memo(() => {
const [openModal, setOpenModal] = useState(false);
const [editing, setEditing] = useState(false);
const { styles } = useStyles();
const [systemRole, updateAgentConfig] = useSessionStore((s) => [
const [systemRole, meta, updateAgentConfig] = useSessionStore((s) => [
agentSelectors.currentAgentSystemRole(s),
agentSelectors.currentAgentMeta(s),
s.updateAgentConfig,
]);
@ -57,6 +59,7 @@ const SystemRole = memo(() => {
<EditableMessage
classNames={{ markdown: styles.prompt }}
editing={editing}
model={{ extra: <AgentInfo meta={meta} style={{ marginBottom: 16 }} /> }}
onChange={(e) => {
updateAgentConfig({ systemRole: e });
}}

View file

@ -1,13 +0,0 @@
import { memo } from 'react';
import { HeaderContent } from '@/app/chat/settings/components/Header';
import Layout from './Desktop';
const Header = memo(() => (
<Layout>
<HeaderContent />
</Layout>
));
export default Header;

View file

@ -1,11 +1,12 @@
import { ChatHeader, ChatHeaderTitle } from '@lobehub/ui';
import { useRouter } from 'next/navigation';
import { ReactNode, memo } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import HeaderContent from '@/app/chat/settings/features/HeaderContent';
import { pathString } from '@/utils/url';
const Header = memo<{ children: ReactNode }>(({ children }) => {
const Header = memo(() => {
const { t } = useTranslation('setting');
const router = useRouter();
@ -13,7 +14,7 @@ const Header = memo<{ children: ReactNode }>(({ children }) => {
<ChatHeader
left={<ChatHeaderTitle title={t('header.session')} />}
onBackClick={() => router.push(pathString('/chat', { hash: location.hash }))}
right={children}
right={<HeaderContent />}
showBackButton
/>
);

View file

@ -5,7 +5,7 @@ import SafeSpacing from '@/components/SafeSpacing';
import { HEADER_HEIGHT } from '@/const/layoutTokens';
import Layout from '../../(desktop)/layout.desktop';
import Header from './Header';
import Header from './features/Header';
export default memo(({ children }: PropsWithChildren) => (
<Layout>

View file

@ -1,12 +0,0 @@
import { memo } from 'react';
import { HeaderContent } from '@/app/chat/settings/components/Header';
import Layout from '@/app/chat/settings/components/Header/Mobile';
const Header = memo(() => (
<Layout>
<HeaderContent />
</Layout>
));
export default Header;

View file

@ -1,11 +1,12 @@
import { MobileNavBar, MobileNavBarTitle } from '@lobehub/ui';
import { useRouter } from 'next/navigation';
import { type ReactNode, memo } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import HeaderContent from '@/app/chat/settings/features/HeaderContent';
import { pathString } from '@/utils/url';
const Header = memo<{ children: ReactNode }>(({ children }) => {
const Header = memo(() => {
const { t } = useTranslation('setting');
const router = useRouter();
@ -13,7 +14,7 @@ const Header = memo<{ children: ReactNode }>(({ children }) => {
<MobileNavBar
center={<MobileNavBarTitle title={t('header.session')} />}
onBackClick={() => router.push(pathString('/chat/mobile', { hash: location.hash }))}
right={children}
right={<HeaderContent />}
showBackButton
/>
);

View file

@ -2,7 +2,7 @@ import { PropsWithChildren, memo } from 'react';
import AppLayoutMobile from '@/layout/AppLayout.mobile';
import Header from './Header';
import Header from './features/Header';
export default memo(({ children }: PropsWithChildren) => (
<AppLayoutMobile navBar={<Header />}>{children}</AppLayoutMobile>

View file

@ -9,9 +9,12 @@ import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
import { exportSingleAgent, exportSingleSession } from '@/helpers/export';
import { useSessionStore } from '@/store/session';
import SubmitAgentButton from './SubmitAgentButton';
export const HeaderContent = memo<{ mobile?: boolean }>(() => {
const { t } = useTranslation('setting');
const id = useSessionStore((s) => s.activeId);
const { mobile } = useResponsive();
const items = useMemo<MenuProps['items']>(
@ -41,8 +44,13 @@ export const HeaderContent = memo<{ mobile?: boolean }>(() => {
const size = mobile ? MOBILE_HEADER_ICON_SIZE : { fontSize: 24 };
return (
<Dropdown arrow={false} menu={{ items }} trigger={['click']}>
<ActionIcon icon={HardDriveDownload} size={size} title={t('export', { ns: 'common' })} />
</Dropdown>
<>
<SubmitAgentButton />
<Dropdown arrow={false} menu={{ items }} trigger={['click']}>
<ActionIcon icon={HardDriveDownload} size={size} title={t('export', { ns: 'common' })} />
</Dropdown>
</>
);
});
export default HeaderContent;

View file

@ -0,0 +1,86 @@
import { Alert, Button, Divider, Input } from 'antd';
import { useTheme } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { kebabCase } from 'lodash-es';
import qs from 'query-string';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import MobilePadding from '@/components/MobilePadding';
import { AGENTS_INDEX_GITHUB_ISSUE } from '@/const/url';
import AgentInfo from '@/features/AgentInfo';
import { useGlobalStore } from '@/store/global';
import { useSessionStore } from '@/store/session';
import { agentSelectors } from '@/store/session/slices/agentConfig';
const Inner = memo(() => {
const { t } = useTranslation('setting');
const [identifier, setIdentifier] = useState('');
const systemRole = useSessionStore(agentSelectors.currentAgentSystemRole);
const theme = useTheme();
const meta = useSessionStore(agentSelectors.currentAgentMeta, isEqual);
const language = useGlobalStore((s) => s.settings.language);
const isMetaPass = Boolean(
meta && meta.title && meta.description && (meta.tags as string[])?.length > 0 && meta.avatar,
);
const handleSubmit = () => {
const body = [
'### systemRole',
systemRole,
'### identifier',
identifier,
'### avatar',
meta.avatar,
'### title',
meta.title,
'### description',
meta.description,
'### tags',
(meta.tags as string[]).join(', '),
'### locale',
language === 'auto' ? navigator.language : language,
].join('\n\n');
const url = qs.stringifyUrl({
query: { body, labels: '🤖 Agent PR', title: `[Agent] ${meta.title}` },
url: AGENTS_INDEX_GITHUB_ISSUE,
});
window.open(url, '_blank');
};
return (
<Flexbox gap={16}>
<MobilePadding>
{!isMetaPass && (
<Alert message={t('submitAgentModal.metaMiss')} showIcon type={'warning'} />
)}
<AgentInfo meta={meta} systemRole={systemRole} />
<Divider style={{ margin: '8px 0' }} />
<strong>
<span style={{ color: theme.colorError, marginRight: 4 }}>*</span>
{t('submitAgentModal.identifier')}
</strong>
<Input
onChange={(e) => setIdentifier(kebabCase(e.target.value))}
placeholder={t('submitAgentModal.placeholder')}
value={identifier}
/>
<Button
block
disabled={!isMetaPass || !identifier}
onClick={handleSubmit}
size={'large'}
type={'primary'}
>
{t('submitAgentModal.button')}
</Button>
</MobilePadding>
</Flexbox>
);
});
export default Inner;

View file

@ -0,0 +1,37 @@
import { ActionIcon, Modal } from '@lobehub/ui';
import { useResponsive } from 'antd-style';
import { Share2 } from 'lucide-react';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
import Inner from './Inner';
const SubmitAgentButton = memo(() => {
const { t } = useTranslation('setting');
const { mobile } = useResponsive();
const [isModalOpen, setIsModalOpen] = useState(false);
const size = mobile ? MOBILE_HEADER_ICON_SIZE : { fontSize: 24 };
return (
<>
<ActionIcon
icon={Share2}
onClick={() => setIsModalOpen(true)}
size={size}
title={t('submitAgentModal.tooltips')}
/>
<Modal
centered={false}
footer={null}
onCancel={() => setIsModalOpen(false)}
open={isModalOpen}
title={t('submitAgentModal.tooltips')}
>
<Inner />
</Modal>
</>
);
});
export default SubmitAgentButton;

View file

@ -0,0 +1,46 @@
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css, token, prefixCls }) => ({
author: css`
font-size: 12px;
`,
avatar: css`
flex: none;
`,
container: css`
position: relative;
padding: 16px 16px 24px;
border-bottom: 1px solid ${token.colorBorderSecondary};
`,
date: css`
font-size: 12px;
color: ${token.colorTextDescription};
`,
desc: css`
color: ${token.colorTextDescription};
text-align: center;
`,
loading: css`
.${prefixCls}-skeleton-content {
display: flex;
flex-direction: column;
}
`,
nav: css`
padding-top: 4px;
.${prefixCls}-tabs-tab {
margin: 4px !important;
+ .${prefixCls}-tabs-tab {
margin: 4px !important;
}
}
`,
title: css`
font-size: 20px;
font-weight: 600;
text-align: center;
`,
}));

View file

@ -1,6 +1,6 @@
import { SearchBar } from '@lobehub/ui';
import { useResponsive } from 'antd-style';
import { memo } from 'react';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMarketStore } from '@/store/market';
@ -8,17 +8,20 @@ import { useMarketStore } from '@/store/market';
const AgentSearchBar = memo(() => {
const { t } = useTranslation('market');
const keywords = useMarketStore((s) => s.searchKeywords);
const [value, setValue] = useState(keywords);
const { mobile } = useResponsive();
return (
<SearchBar
allowClear
enableShortKey={!mobile}
onChange={(e) => useMarketStore.setState({ searchKeywords: e.target.value })}
onChange={(e) => setValue(e.target.value)}
onPressEnter={() => useMarketStore.setState({ searchKeywords: value })}
onSubmit={() => useMarketStore.setState({ searchKeywords: value })}
placeholder={t('search.placeholder')}
shortKey={'k'}
spotlight={!mobile}
type={mobile ? 'block' : 'ghost'}
value={keywords}
value={value}
/>
);
});

View file

@ -4,29 +4,24 @@ import { OpenAIChatStreamPayload } from '@/types/openai/chat';
export const promptSummaryAgentName = (content: string): Partial<OpenAIChatStreamPayload> => ({
messages: [
{
content: `你是一名擅长起名的起名大师,你需要将用户的描述总结为 20 个字以内的角色,格式要求如下:
: {JSON引用字符串}
: {}
`,
content: `你是一名擅长起名的起名大师,名字需要有文学内涵,注重精炼和赋子意境,你需要将用户的描述总结为 20 个字以内的角色, 格式要求如下:\n输入: {文本作为JSON引用字符串}\n输出: {角色名}`,
role: 'system',
},
{
content: `输入: {你是一名专业的前端开发者,擅长结合 vitest 和\`testing-library/react\` 书写单元测试。接下来用户会输入一串 ts 代码,你需要给出完善的单元测试。\n你需要注意单元测试代码中不应该使用 jest 。如果需要使用 \`jest.fn\`,请使用 \`vi.fn\` 替换}`,
content: `输入: {你是一名文案大师,帮我为一些设计 / 艺术作品起名,名字需要有文学内涵,注重精炼和赋子意境,表达作品的情景氛国,使名称既简洁又富有诗意。}`,
role: 'user',
},
{ content: '前端 vitest 测试专家', role: 'assistant' },
{ content: '创意命名师', role: 'assistant' },
{
content: `输入: {你是一名前端专家,请将下面的代码转成 ts不要修改实现。如果原本 js 中没有定义的全局变量,需要补充 declare 的类型声明。}`,
content: `输入: {你是一名前端代码专家,请将下面的代码转成 ts不要修改实现。如果原本 js 中没有定义的全局变量,需要补充 declare 的类型声明。}`,
role: 'user',
},
{ content: 'js 转 ts 专家', role: 'assistant' },
{ content: 'ts转换魔术师', role: 'assistant' },
{
content: `输入:{你是一名擅长比喻和隐喻的UX Writter。用户会输入文案你需要给出3个优化后的结果使用 markdown格式的文本。下面是一个例子
}`,
content: `输入: {你是一名创业计划撰写专家,可以提供包括创意名称、简短的标语、目标用户画像、用户痛点、主要价值主张、销售/营销渠道、收入流、成本结构等计划生成。}`,
role: 'user',
},
{ content: '文案比喻优化专家', role: 'assistant' },
{ content: '创业咨询专家', role: 'assistant' },
{ content: `输入: {${content}}`, role: 'user' },
],
});
@ -35,42 +30,76 @@ export const promptSummaryAgentName = (content: string): Partial<OpenAIChatStrea
export const promptPickEmoji = (content: string): Partial<OpenAIChatStreamPayload> => ({
messages: [
{
content: '你是一名非常懂设计与时尚的设计师,你需要从用户的描述中匹配一个合适的 emoji。',
content:
'你是一名擅长进行概念抽象的设计师与 Emoji 专家,你需要根据角色能力的描述抽象出一个表达物理实体的概念 Emoji 作为角色头像, 格式要求如下:\n输入: {文本作为JSON引用字符串}\n输出: {一个Emoji}',
role: 'system',
},
{
content: `输入:你是一名精通体验设计的设计系统设计师,设计系统存在诸多类别的 token比如品牌色、成功色等你需要为各个类别的 token 提供说明文案。`,
content: `输入: {你是一名文案大师,帮我为一些设计 / 艺术作品起名,名字需要有文学内涵,注重精炼和赋子意境,表达作品的情景氛国,使名称既简洁又富有诗意。}`,
role: 'user',
},
{ content: '✒️', role: 'assistant' },
{
content: `💅`,
role: 'assistant',
},
{
content: `输入:用户会输入一串 ts 代码,为了确保所有功能和分支的 100% 的覆盖率,你需要给出需要考虑哪些数据场景。`,
content: `输入: {你是一名代码巫师,请将下面的代码转成 ts不要修改实现。如果原本 js 中没有定义的全局变量,需要补充 declare 的类型声明。}`,
role: 'user',
},
{ content: '🧙‍♂️', role: 'assistant' },
{
content: `🧪`,
role: 'assistant',
},
{
content: `输入:${content}`,
content: `输入: {你是一名创业计划撰写专家,可以提供包括创意名称、简短的标语、目标用户画像、用户痛点、主要价值主张、销售/营销渠道、收入流、成本结构等计划生成。}`,
role: 'user',
},
{ content: '🚀', role: 'assistant' },
{ content: `输入: {${content}}`, role: 'user' },
],
});
export const promptSummaryDescription = (content: string): Partial<OpenAIChatStreamPayload> => ({
messages: [
{
content:
'你是一名擅长会话的助理,你需要将用户的输入的内容总结为一个专家的简介,不超过 20 个字',
content: `你是一名擅长技能总结的助理,你需要将用户的输入的内容总结为一个角色技能简介,确保信息清晰、逻辑清晰,并有效地传达角色的技能和经验,不超过 20 个字, 格式要求如下:\n输入: {文本作为JSON引用字符串}\n输出: {角色技能简介}`,
role: 'system',
},
{
content: `${content}`,
content: `输入: {你是一名文案大师,帮我为一些设计 / 艺术作品起名,名字需要有文学内涵,注重精炼和赋子意境,表达作品的情景氛国,使名称既简洁又富有诗意。}`,
role: 'user',
},
{ content: '擅长文创艺术作品起名', role: 'assistant' },
{
content: `输入: {你是一名前端代码专家,请将下面的代码转成 ts不要修改实现。如果原本 js 中没有定义的全局变量,需要补充 declare 的类型声明。}`,
role: 'user',
},
{ content: '擅长 ts 转换和补充类型声明', role: 'assistant' },
{
content: `输入: {你是一名创业计划撰写专家,可以提供包括创意名称、简短的标语、目标用户画像、用户痛点、主要价值主张、销售/营销渠道、收入流、成本结构等计划生成。}`,
role: 'user',
},
{ content: '擅长创业计划撰写与咨询', role: 'assistant' },
{ content: `输入: {${content}}`, role: 'user' },
],
});
export const promptSummaryTags = (content: string): Partial<OpenAIChatStreamPayload> => ({
messages: [
{
content:
'你是一名擅长会话标签总结的助理,你需要将用户的输入的内容提炼出分类标签,使用`,`分隔,不超过 5 个标签, 格式要求如下:\n输入: {文本作为JSON引用字符串}\n输出: {角色名}',
role: 'system',
},
{
content: `输入: {你是一名文案大师,帮我为一些设计 / 艺术作品起名,名字需要有文学内涵,注重精炼和赋子意境,表达作品的情景氛国,使名称既简洁又富有诗意。}`,
role: 'user',
},
{ content: '起名,写作,创意', role: 'assistant' },
{
content: `输入: {你是一名前端专家,请将下面的代码转成 ts不要修改实现。如果原本 js 中没有定义的全局变量,需要补充 declare 的类型声明。}`,
role: 'user',
},
{ content: '代码,软件开发,效率', role: 'assistant' },
{
content: `输入: {你是一名创业计划撰写专家,可以提供包括创意名称、简短的标语、目标用户画像、用户痛点、主要价值主张、销售/营销渠道、收入流、成本结构等计划生成。}`,
role: 'user',
},
{ content: '创业,计划,咨询', role: 'assistant' },
{ content: `输入: {${content}}`, role: 'user' },
],
});

View file

@ -47,9 +47,9 @@ const HotKeys = memo<HotKeysProps>(({ keys, desc }) => {
if (!desc) return content;
return (
<Flexbox align={'center'} gap={8} horizontal>
{content}
<Flexbox align={'center'} style={{ textAlign: 'center' }}>
{desc}
{content}
</Flexbox>
);
});

View file

@ -14,10 +14,12 @@ const ResponsiveIndex = ({ children, Mobile }: ResponsiveIndexProps) => {
const { t } = useTranslation();
const mobile = useIsMobile();
return (
return mobile ? (
<Suspense fallback={<FullscreenLoading title={t('layoutInitializing', { ns: 'common' })} />}>
{mobile ? <Mobile /> : children}
<Mobile />
</Suspense>
) : (
children
);
};

View file

@ -43,6 +43,7 @@ export const getAgentJSON = (
};
export const AGENTS_INDEX_GITHUB = 'https://github.com/lobehub/lobe-chat-agents';
export const AGENTS_INDEX_GITHUB_ISSUE = urlJoin(AGENTS_INDEX_GITHUB, 'issues/new');
export const SESSION_CHAT_URL = (id: string = INBOX_SESSION_ID, mobile?: boolean) =>
mobile ? `/chat/mobile#session=${id}` : `/chat#session=${id}`;

View file

@ -0,0 +1,69 @@
import { Avatar, Markdown, Tag } from '@lobehub/ui';
import { Divider } from 'antd';
import { createStyles } from 'antd-style';
import { startCase } from 'lodash-es';
import { CSSProperties, memo } from 'react';
import { Center } from 'react-layout-kit';
import { MetaData } from '@/types/meta';
const useStyles = createStyles(({ css, token }) => ({
avatar: css`
flex: none;
`,
desc: css`
color: ${token.colorTextDescription};
text-align: center;
`,
title: css`
font-size: 20px;
font-weight: 600;
text-align: center;
`,
}));
export interface AgentInfoProps {
meta?: MetaData;
style?: CSSProperties;
systemRole?: string;
}
const AgentInfo = memo<AgentInfoProps>(({ systemRole, style, meta }) => {
const { styles, theme } = useStyles();
if (!meta) return;
return (
<Center gap={16} style={style}>
{meta.avatar && (
<Avatar
animation
avatar={meta.avatar}
background={meta.backgroundColor || theme.colorFillTertiary}
className={styles.avatar}
size={100}
/>
)}
{meta.title && <div className={styles.title}>{meta.title}</div>}
{(meta?.tags as string[])?.length > 0 && (
<Center gap={6} horizontal style={{ flexWrap: 'wrap' }}>
{(meta.tags as string[]).map((tag: string, index) => (
<Tag key={index} style={{ margin: 0 }}>
{startCase(tag).trim()}
</Tag>
))}
</Center>
)}
{meta.description && <div className={styles.desc}>{meta.description}</div>}
{systemRole && (
<>
<Divider style={{ margin: '8px 0' }} />
<Markdown>{systemRole}</Markdown>
</>
)}
</Center>
);
});
export default AgentInfo;

View file

@ -0,0 +1,48 @@
import { ActionIcon } from '@lobehub/ui';
import { Select, SelectProps } from 'antd';
import { useTheme } from 'antd-style';
import { isString } from 'lodash-es';
import { Wand2 } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export interface AutoGenerateInputProps extends SelectProps {
loading?: boolean;
onGenerate?: () => void;
}
const AutoGenerateSelect = memo<AutoGenerateInputProps>(
({ loading, onGenerate, value, ...props }) => {
const { t } = useTranslation('common');
const theme = useTheme();
return (
<Select
mode="tags"
open={false}
style={{ width: '100%' }}
suffixIcon={
onGenerate && (
<ActionIcon
active
icon={Wand2}
loading={loading}
onClick={onGenerate}
size={'small'}
style={{
color: theme.colorInfo,
marginRight: -4,
}}
title={t('autoGenerate')}
/>
)
}
tokenSeparators={[',', '', ' ']}
value={isString(value) ? value.split(',') : value}
{...props}
/>
);
},
);
export default AutoGenerateSelect;

View file

@ -1,6 +1,7 @@
import { Form, type FormItemProps, Icon, type ItemGroup, Tooltip } from '@lobehub/ui';
import { Button } from 'antd';
import isEqual from 'fast-deep-equal';
import { isString } from 'lodash-es';
import { UserCircle, Wand2 } from 'lucide-react';
import dynamic from 'next/dynamic';
import { memo, useMemo } from 'react';
@ -11,6 +12,7 @@ import { FORM_STYLE } from '@/const/layoutTokens';
import { useStore } from '../store';
import { SessionLoadingState } from '../store/initialState';
import AutoGenerateInput from './AutoGenerateInput';
import AutoGenerateSelect from './AutoGenerateSelect';
import BackgroundSwatches from './BackgroundSwatches';
const EmojiPicker = dynamic(() => import('@/components/EmojiPicker'), { ssr: false });
@ -28,26 +30,35 @@ const AgentMeta = memo(() => {
const basic = [
{
Render: AutoGenerateInput,
key: 'title',
label: t('settingAgent.name.title'),
onChange: (e: any) => updateMeta({ title: e.target.value }),
placeholder: t('settingAgent.name.placeholder'),
},
{
Render: AutoGenerateInput,
key: 'description',
label: t('settingAgent.description.title'),
onChange: (e: any) => updateMeta({ description: e.target.value }),
placeholder: t('settingAgent.description.placeholder'),
},
// { key: 'tag', label: t('agentTag'), placeholder: t('agentTagPlaceholder') },
{
Render: AutoGenerateSelect,
key: 'tags',
label: t('settingAgent.tag.title'),
onChange: (e: any) => updateMeta({ tags: isString(e) ? e.split(',') : e }),
placeholder: t('settingAgent.tag.placeholder'),
},
];
const autocompleteItems: FormItemProps[] = basic.map((item) => {
const AutoGenerate = item.Render;
return {
children: (
<AutoGenerateInput
<AutoGenerate
loading={loading[item.key as keyof SessionLoadingState]}
onChange={(e) => {
updateMeta({ [item.key]: e.target.value });
}}
onChange={item.onChange}
onGenerate={() => {
autocompleteMeta(item.key as keyof typeof meta);
}}

View file

@ -1,6 +1,11 @@
import { StateCreator } from 'zustand/vanilla';
import { promptPickEmoji, promptSummaryAgentName, promptSummaryDescription } from '@/chains/agent';
import {
promptPickEmoji,
promptSummaryAgentName,
promptSummaryDescription,
promptSummaryTags,
} from '@/chains/agent';
import { MetaData } from '@/types/meta';
import { LobeAgentConfig } from '@/types/session';
import { fetchPresetTaskResult } from '@/utils/fetch';
@ -26,6 +31,7 @@ export interface Action {
* @returns Promise
*/
autocompleteAgentDescription: () => Promise<void>;
autocompleteAgentTags: () => Promise<void>;
/**
*
* @param id - ID
@ -47,7 +53,8 @@ export interface Action {
setAgentConfig: (config: Partial<LobeAgentConfig>) => void;
setAgentMeta: (meta: Partial<MetaData>) => void;
streamUpdateMeta: (key: keyof MetaData) => any;
streamUpdateMetaArray: (key: keyof MetaData) => any;
streamUpdateMetaString: (key: keyof MetaData) => any;
toggleAgentPlugin: (pluginId: string, state?: boolean) => void;
/**
*
@ -65,7 +72,7 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
...initialState,
autoPickEmoji: async () => {
const { config, dispatchMeta } = get();
const { config, meta, dispatchMeta } = get();
const systemRole = config.systemRole;
@ -73,7 +80,7 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
onLoadingChange: (loading) => {
get().updateLoadingState('avatar', loading);
},
params: promptPickEmoji(systemRole),
params: promptPickEmoji([meta.title, meta.description, systemRole].filter(Boolean).join(',')),
});
if (emoji) {
@ -81,7 +88,7 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
}
},
autocompleteAgentDescription: async () => {
const { dispatchMeta, config, meta, updateLoadingState, streamUpdateMeta } = get();
const { dispatchMeta, config, meta, updateLoadingState, streamUpdateMetaString } = get();
const systemRole = config.systemRole;
@ -99,12 +106,37 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
onLoadingChange: (loading) => {
updateLoadingState('description', loading);
},
onMessageHandle: streamUpdateMeta('description'),
onMessageHandle: streamUpdateMetaString('description'),
params: promptSummaryDescription(systemRole),
});
},
autocompleteAgentTags: async () => {
const { dispatchMeta, config, meta, updateLoadingState, streamUpdateMetaArray } = get();
const systemRole = config.systemRole;
if (!systemRole) return;
const preValue = meta.tags;
// 替换为 ...
dispatchMeta({ type: 'update', value: { tags: ['...'] } });
fetchPresetTaskResult({
onError: () => {
dispatchMeta({ type: 'update', value: { tags: preValue } });
},
onLoadingChange: (loading) => {
updateLoadingState('tags', loading);
},
onMessageHandle: streamUpdateMetaArray('tags'),
params: promptSummaryTags(
[meta.title, meta.description, systemRole].filter(Boolean).join(','),
),
});
},
autocompleteAgentTitle: async () => {
const { dispatchMeta, config, meta, updateLoadingState, streamUpdateMeta } = get();
const { dispatchMeta, config, meta, updateLoadingState, streamUpdateMetaString } = get();
const systemRole = config.systemRole;
@ -122,8 +154,8 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
onLoadingChange: (loading) => {
updateLoadingState('title', loading);
},
onMessageHandle: streamUpdateMeta('title'),
params: promptSummaryAgentName(systemRole),
onMessageHandle: streamUpdateMetaString('title'),
params: promptSummaryAgentName([meta.description, systemRole].filter(Boolean).join(',')),
});
},
autocompleteAllMeta: (replace) => {
@ -140,9 +172,18 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
if (!meta.avatar || replace) {
get().autoPickEmoji();
}
if (!meta.tags || replace) {
get().autocompleteAgentTags();
}
},
autocompleteMeta: (key) => {
const { autoPickEmoji, autocompleteAgentTitle, autocompleteAgentDescription } = get();
const {
autoPickEmoji,
autocompleteAgentTitle,
autocompleteAgentDescription,
autocompleteAgentTags,
} = get();
switch (key) {
case 'avatar': {
@ -157,6 +198,12 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
case 'title': {
autocompleteAgentTitle();
return;
}
case 'tags': {
autocompleteAgentTags();
return;
}
}
},
@ -187,10 +234,19 @@ export const store: StateCreator<Store, [['zustand/devtools', never]]> = (set, g
get().dispatchConfig({ config, type: 'update' });
},
setAgentMeta: (meta) => {
console.log(meta);
get().dispatchMeta({ type: 'update', value: meta });
},
streamUpdateMeta: (key: keyof MetaData) => {
streamUpdateMetaArray: (key: keyof MetaData) => {
let value = '';
return (text: string) => {
value += text;
get().dispatchMeta({ type: 'update', value: { [key]: value.split(',') } });
};
},
streamUpdateMetaString: (key: keyof MetaData) => {
let value = '';
return (text: string) => {
value += text;

View file

@ -36,7 +36,6 @@ export default {
withSystemRole: '包含助手角色设定',
},
stop: '停止',
temp: '临时',
tokenDetail: '角色设定: {{systemRoleToken}} · 历史消息: {{chatsToken}}',
tokenTag: {

View file

@ -109,6 +109,7 @@ export default {
},
title: '助手信息',
},
settingChat: {
chatStyleType: {
title: '聊天窗口样式',
@ -222,6 +223,13 @@ export default {
},
title: '主题设置',
},
submitAgentModal: {
button: '提交助手',
identifier: 'identifier 助手标识符',
metaMiss: '请补全助手信息后提交,需要包含名称、描述和标签',
placeholder: '请输入助手的标识符,需要是唯一的,比如 web-development',
tooltips: '分享到助手市场',
},
tab: {
agent: '默认助手',
common: '通用设置',

View file

@ -13,7 +13,6 @@ const resources = {
'zh-CN': zh_CN,
'zh-TW': zh_TW,
} as const;
export default resources;
export const defaultResources = zh_CN;
export type Resources = typeof resources;

View file

@ -112,7 +112,7 @@ export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch):
const { key, value } = payload;
const validKeys = ['avatar', 'backgroundColor', 'description', 'tag', 'title'];
const validKeys = ['avatar', 'backgroundColor', 'description', 'tags', 'title'];
if (validKeys.includes(key)) chat.meta[key] = value;
});