🔧 chore: Update setting

This commit introduces new components, modules, and features related to chat, sessions, and settings. It includes modifications to configuration files, updates to dependencies, adjustments to styles and layouts, and additions of new components and modules. The changes also involve updates to functions, interfaces, selectors, actions, and reducers. Additionally, there are modifications to TypeScript interfaces and types, as well as changes to parameters and functions in certain files.
This commit is contained in:
canisminor1990 2023-07-15 15:48:20 +08:00
parent 30da537618
commit 4e522a6e2b
72 changed files with 631 additions and 447 deletions

1
.changelogrc.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('@lobehub/lint').changelog;

1
.commitlintrc.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('@lobehub/lint').commitlint;

16
.editorconfig Normal file
View file

@ -0,0 +1,16 @@
# http://editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

32
.eslintignore Normal file
View file

@ -0,0 +1,32 @@
# Eslintignore for LobeHub
################################################################
# dependencies
node_modules
# ci
coverage
.coverage
# test
jest*
_test_
__test__
*.test.ts
# umi
.umi
.umi-production
.umi-test
.dumi/tmp*
!.dumirc.ts
# production
dist
es
lib
logs
# misc
# add other ignore file below
.next

View file

@ -3,13 +3,4 @@ const config = require('@lobehub/lint').eslint;
config.extends.push('plugin:@next/next/recommended');
//config.extends.push('plugin:@next/next/core-web-vitals');
module.exports = {
...config,
rules: {
...config.rules,
'react/jsx-sort-props': 'off',
'sort-keys-fix/sort-keys-fix': 'off',
'typescript-sort-keys/interface': 'off',
'unicorn/switch-case-braces': 'off',
},
};
module.exports = config;

View file

@ -1,11 +1,11 @@
name: "🐛 反馈缺陷 Bug Report"
description: "反馈一个问题缺陷 | Report an bug"
title: "[Bug] "
labels: "🐛 Bug"
name: '🐛 反馈缺陷 Bug Report'
description: '反馈一个问题缺陷 | Report an bug'
title: '[Bug] '
labels: '🐛 Bug'
body:
- type: dropdown
attributes:
label: "💻 系统环境 | Operating System"
label: '💻 系统环境 | Operating System'
options:
- Windows
- macOS
@ -16,7 +16,7 @@ body:
required: true
- type: dropdown
attributes:
label: "🌐 浏览器 | Browser"
label: '🌐 浏览器 | Browser'
options:
- Chrome
- Edge
@ -27,19 +27,19 @@ body:
required: true
- type: textarea
attributes:
label: "🐛 问题描述 | Bug Description"
label: '🐛 问题描述 | Bug Description'
description: A clear and concise description of the bug.
validations:
required: true
- type: textarea
attributes:
label: "🚦 期望结果 | Expected Behavior"
label: '🚦 期望结果 | Expected Behavior'
description: A clear and concise description of what you expected to happen.
- type: textarea
attributes:
label: "📷 复现步骤 | Recurrence Steps"
label: '📷 复现步骤 | Recurrence Steps'
description: A clear and concise description of how to recurrence.
- type: textarea
attributes:
label: "📝 补充信息 | Additional Information"
label: '📝 补充信息 | Additional Information'
description: If your problem needs further explanation, or if the issue you're seeing cannot be reproduced in a gist, please add more information here.

View file

@ -1,21 +1,21 @@
name: "🌠 功能需求 Feature Request"
description: "需求或建议 | Suggest an idea"
title: "[Request] "
labels: "🌠 Feature Request"
name: '🌠 功能需求 Feature Request'
description: '需求或建议 | Suggest an idea'
title: '[Request] '
labels: '🌠 Feature Request'
body:
- type: textarea
attributes:
label: "🥰 需求描述 | Feature Description"
label: '🥰 需求描述 | Feature Description'
description: Please add a clear and concise description of the problem you are seeking to solve with this feature request.
validations:
required: true
- type: textarea
attributes:
label: "🧐 解决方案 | Proposed Solution"
label: '🧐 解决方案 | Proposed Solution'
description: Describe the solution you'd like in a clear and concise manner.
validations:
required: true
- type: textarea
attributes:
label: "📝 补充信息 | Additional Information"
label: '📝 补充信息 | Additional Information'
description: Add any other context about the problem here.

View file

@ -1,15 +1,15 @@
name: "😇 疑问或帮助 Help Wanted"
description: "疑问或需要帮助 | Need help"
title: "[Question] "
labels: "😇 Help Wanted"
name: '😇 疑问或帮助 Help Wanted'
description: '疑问或需要帮助 | Need help'
title: '[Question] '
labels: '😇 Help Wanted'
body:
- type: textarea
attributes:
label: "🧐 问题描述 | Proposed Solution"
label: '🧐 问题描述 | Proposed Solution'
description: A clear and concise description of the proplem.
validations:
required: true
- type: textarea
attributes:
label: "📝 补充信息 | Additional Information"
label: '📝 补充信息 | Additional Information'
description: Add any other context about the problem here.

View file

@ -1,17 +1,17 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
directory: '/'
schedule:
interval: weekly
time: "19:00"
time: '19:00'
timezone: 'Asia/Shanghai'
open-pull-requests-limit: 10
versioning-strategy: increase
- package-ecosystem: "github-actions"
directory: "/"
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: monthly
time: "19:00"
time: '19:00'
timezone: 'Asia/Shanghai'

View file

@ -2,7 +2,7 @@ name: Contributor Helper
on:
# 🌏 Think about the planet! No need to update stats too frequently
schedule: [{cron: "0 18 * * *"}]
schedule: [{ cron: '0 18 * * *' }]
# 💡 The following line lets you run workflow manually from the action tab!
workflow_dispatch:
jobs:

View file

@ -2,7 +2,7 @@ name: Issue Check Inactive
on:
schedule:
- cron: "0 0 */15 * *"
- cron: '0 0 */15 * *'
permissions:
contents: read
@ -10,8 +10,8 @@ permissions:
jobs:
issue-check-inactive:
permissions:
issues: write # for actions-cool/issues-helper to update issues
pull-requests: write # for actions-cool/issues-helper to update PRs
issues: write # for actions-cool/issues-helper to update issues
pull-requests: write # for actions-cool/issues-helper to update PRs
runs-on: ubuntu-latest
steps:
- name: check-inactive

View file

@ -2,7 +2,7 @@ name: Issue Close Require
on:
schedule:
- cron: "0 0 * * *"
- cron: '0 0 * * *'
permissions:
contents: read
@ -10,8 +10,8 @@ permissions:
jobs:
issue-close-require:
permissions:
issues: write # for actions-cool/issues-helper to update issues
pull-requests: write # for actions-cool/issues-helper to update PRs
issues: write # for actions-cool/issues-helper to update issues
pull-requests: write # for actions-cool/issues-helper to update PRs
runs-on: ubuntu-latest
steps:
- name: need reproduce

View file

@ -12,8 +12,8 @@ permissions:
jobs:
issue-remove-inactive:
permissions:
issues: write # for actions-cool/issues-helper to update issues
pull-requests: write # for actions-cool/issues-helper to update PRs
issues: write # for actions-cool/issues-helper to update issues
pull-requests: write # for actions-cool/issues-helper to update PRs
runs-on: ubuntu-latest
steps:
- name: remove inactive

1
.gitignore vendored
View file

@ -53,4 +53,3 @@ test-output
*.tsbuildinfo
next-env.d.ts
.next
.env

3
.gitpod.yml Normal file
View file

@ -0,0 +1,3 @@
tasks:
- init: pnpm install
command: pnpm run start

9
.npmrc
View file

@ -1,2 +1,11 @@
lockfile=false
resolution-mode=highest
public-hoist-pattern[]=*@umijs/lint*
public-hoist-pattern[]=*changelog*
public-hoist-pattern[]=*commitlint*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=*postcss*
public-hoist-pattern[]=*prettier*
public-hoist-pattern[]=*remark*
public-hoist-pattern[]=*semantic-release*
public-hoist-pattern[]=*stylelint*

View file

@ -1,28 +1,63 @@
**/*.svg
.umi
.umi-production
/dist
.dockerignore
# Prettierignore for LobeHub
################################################################
# general
.DS_Store
.eslintignore
*.png
*.jpg
*.webp
*.toml
*.py
docker
.editorconfig
Dockerfile*
.gitignore
.prettierignore
LICENSE
.eslintcache
*.lock
yarn-error.log
.idea
.vscode
.history
.temp
.env.local
.husky
.npmrc
.env.local
.next
.gitkeep
venv
temp
tmp
LICENSE
# dependencies
node_modules
*.log
*.lock
package-lock.json
# ci
coverage
.coverage
.eslintcache
.stylelintcache
test-output
__snapshots__
.snap
*.snap
# production
dist
es
lib
logs
# umi
.umi
.umi-production
.umi-test
.dumi/tmp*
# ignore files
.*ignore
# docker
docker
Dockerfile*
# image
*.webp
*.gif
*.png
*.jpg
*.svg
# misc
# add other ignore file below
.next

View file

@ -1,10 +1 @@
module.exports = {
printWidth: 120,
singleQuote: true,
trailingComma: 'all',
proseWrap: 'never',
endOfLine: 'lf',
overrides: [{ files: '.prettierrc', options: { parser: 'json' } }],
plugins: [require.resolve('prettier-plugin-packagejson'), require.resolve('prettier-plugin-organize-imports')],
pluginSearchDirs: false,
};
module.exports = require('@lobehub/lint').prettier;

View file

@ -1,4 +1 @@
module.exports = {
extends: ['semantic-release-config-gitmoji'],
branches: ['master'],
};
module.exports = require('@lobehub/lint').semanticRelease;

1
.remarkrc.js Normal file
View file

@ -0,0 +1 @@
module.exports = require('@lobehub/lint').remarklint;

View file

@ -1,10 +1,8 @@
const config = require('@lobehub/lint').stylelint;
module.exports = {
extends: ['stylelint-config-recommended', 'stylelint-config-clean-order'],
files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
plugins: ['stylelint-order'],
customSyntax: 'postcss-styled-syntax',
...config,
rules: {
'no-empty-source': null,
'no-invalid-double-slash-comments': null,
'selector-id-pattern': null,
},
};

View file

@ -51,7 +51,7 @@ Or clone it for local development:
```bash
$ git clone https://github.com/lobehub/lobe-chat.git
$ cd canisminor-template
$ cd lobe-chat
$ pnpm install
$ pnpm start
```

View file

@ -22,7 +22,7 @@
"sideEffects": false,
"scripts": {
"build": "next build",
"dev": "PORT=3010 next dev",
"dev": "next dev -p 3010",
"lint": "eslint \"{src,tests}/**/*.{js,jsx,ts,tsx}\" --fix",
"lint:md": "remark . --quiet --frail --output",
"lint:style": "stylelint \"{src,tests}/**/*.{js,jsx,ts,tsx}\" --fix",
@ -88,7 +88,7 @@
},
"devDependencies": {
"@commitlint/cli": "^17",
"@lobehub/lint": "^1",
"@lobehub/lint": "latest",
"@next/eslint-plugin-next": "^13",
"@testing-library/jest-dom": "^5",
"@testing-library/react": "^14",
@ -110,8 +110,8 @@
"node-fetch": "^3",
"postcss-styled-syntax": "^0.4",
"prettier": "^2",
"prettier-plugin-organize-imports": "^3",
"prettier-plugin-packagejson": "^2",
"remark": "^14",
"remark-cli": "^11",
"semantic-release": "^21",
"semantic-release-config-gitmoji": "^1",
"stylelint": "^15",

View file

@ -15,17 +15,26 @@ export const useStyles = createStyles(({ css, token }) => ({
export default ({ children }: PropsWithChildren) => {
const { styles } = useStyles();
const [sessionsWidth, sessionExpandable] = useSettings((s) => [s.sessionsWidth, s.sessionExpandable], shallow);
const [sessionsWidth, sessionExpandable] = useSettings(
(s) => [s.sessionsWidth, s.sessionExpandable],
shallow,
);
const [tmpWidth, setWidth] = useState(sessionsWidth);
if (tmpWidth !== sessionsWidth) setWidth(sessionsWidth);
return (
<DraggablePanel
placement="left"
className={styles.panel}
defaultSize={{ width: tmpWidth }}
expand={sessionExpandable}
maxWidth={400}
minWidth={256}
defaultSize={{ width: tmpWidth }}
size={{ width: sessionsWidth, height: '100vh' }}
onExpandChange={(expand) => {
useSettings.setState({
sessionExpandable: expand,
sessionsWidth: expand ? 320 : 0,
});
}}
onSizeChange={(_, size) => {
if (!size) return;
@ -36,14 +45,8 @@ export default ({ children }: PropsWithChildren) => {
setWidth(nextWidth);
useSettings.setState({ sessionsWidth: nextWidth });
}}
expand={sessionExpandable}
onExpandChange={(expand) => {
useSettings.setState({
sessionsWidth: expand ? 320 : 0,
sessionExpandable: expand,
});
}}
className={styles.panel}
placement="left"
size={{ height: '100vh', width: sessionsWidth }}
>
{children}
</DraggablePanel>

View file

@ -1,6 +1,7 @@
import { getInputVariablesFromMessages } from '@/helpers/prompt';
import { ChatMessage } from '@lobehub/ui';
import { getInputVariablesFromMessages } from '@/helpers/prompt';
describe('getInputVariablesFromMessages 方法', () => {
it('应当在输入为空数组时返回空数组', () => {
const result = getInputVariablesFromMessages([]);

View file

@ -11,14 +11,17 @@ export const getChatPromptTemplate = (chatMessages: ChatMessage[]) =>
chatMessages.map((m) => {
switch (m.role) {
default:
case 'user':
case 'user': {
return HumanMessagePromptTemplate.fromTemplate(m.content);
}
case 'system':
case 'system': {
return SystemMessagePromptTemplate.fromTemplate(m.content);
}
case 'assistant':
case 'assistant': {
return AIMessagePromptTemplate.fromTemplate(m.content);
}
}
}),
);

View file

@ -1,8 +1,11 @@
import { Compressor } from '@/utils/compass';
import { ChatMessage } from '@lobehub/ui';
import { Compressor } from '@/utils/compass';
export const genShareMessagesUrl = (messages: ChatMessage[], systemRole?: string) => {
const compassedMsg = systemRole ? [{ role: 'system', content: systemRole }, ...messages] : messages;
const compassedMsg = systemRole
? [{ content: systemRole, role: 'system' }, ...messages]
: messages;
return `/share?messages=${Compressor.compress(JSON.stringify(compassedMsg))}`;
};

View file

@ -1,11 +1,10 @@
import { ThemeProvider } from '@lobehub/ui';
import { App, ConfigProvider } from 'antd';
import 'antd/dist/reset.css';
import Zh_CN from 'antd/locale/zh_CN';
import { PropsWithChildren, useEffect } from 'react';
import { useChatStore } from 'src/store/session';
import { GlobalStyle, useStyles } from './style';
const Layout = ({ children }: PropsWithChildren) => {

View file

@ -16,7 +16,7 @@ export const useStyles = createStyles(({ css, token }) => ({
background-image: linear-gradient(
180deg,
${token.colorBgContainer} 0%,
rgba(255, 255, 255, 0) 20%
rgba(255, 255, 255, 0%) 20%
);
:has(#ChatLayout, #FlowLayout) {

View file

@ -1,7 +1,8 @@
import Layout from '@/layout';
import { Analytics } from '@vercel/analytics/react';
import type { AppProps } from 'next/app';
import Layout from '@/layout';
function MyApp({ Component, pageProps }: AppProps) {
return (
<Layout>

View file

@ -1,4 +1,4 @@
import { extractStaticStyle, StyleProvider } from 'antd-style';
import { StyleProvider, extractStaticStyle } from 'antd-style';
import Document, { DocumentContext, Head, Html, Main, NextScript } from 'next/document';
class MyDocument extends Document {

View file

@ -1,4 +1,3 @@
import { LangChainParams } from '@/types/langchain';
import { LLMChain } from 'langchain/chains';
import { ChatOpenAI } from 'langchain/chat_models/openai';
import {
@ -8,6 +7,8 @@ import {
SystemMessagePromptTemplate,
} from 'langchain/prompts';
import { LangChainParams } from '@/types/langchain';
const isDev = process.env.NODE_ENV === 'development';
const OPENAI_PROXY_URL = process.env.OPENAI_PROXY_URL;
@ -19,13 +20,16 @@ export function LangChainStream(payload: LangChainParams) {
prompts.map((m) => {
switch (m.role) {
default:
case 'user':
case 'user': {
return HumanMessagePromptTemplate.fromTemplate(m.content);
case 'system':
}
case 'system': {
return SystemMessagePromptTemplate.fromTemplate(m.content);
}
case 'assistant':
case 'assistant': {
return AIMessagePromptTemplate.fromTemplate(m.content);
}
}
}),
);
@ -43,8 +47,7 @@ export function LangChainStream(payload: LangChainParams) {
{
streaming: true,
...llm,
// 暂时设定不重试 ,后续看是否需要支持重试
maxRetries: 0,
callbacks: [
{
handleLLMNewToken(token) {
@ -60,14 +63,13 @@ export function LangChainStream(payload: LangChainParams) {
},
},
],
// 暂时设定不重试 ,后续看是否需要支持重试
maxRetries: 0,
},
isDev && OPENAI_PROXY_URL ? { basePath: OPENAI_PROXY_URL } : undefined,
);
const chain = new LLMChain({
prompt: chatPrompt,
llm: chat,
verbose: true,
callbacks: [
{
handleChainError(err: Error): Promise<void> | void {
@ -75,6 +77,9 @@ export function LangChainStream(payload: LangChainParams) {
},
},
],
llm: chat,
prompt: chatPrompt,
verbose: true,
});
try {
// 使用转换后的聊天消息作为输入开始聊天

View file

@ -1,7 +1,8 @@
import { OpenAIChatMessage } from '@/types/openai';
import { ChatOpenAI } from 'langchain/chat_models/openai';
import { AIChatMessage, HumanChatMessage, SystemChatMessage } from 'langchain/schema';
import { OpenAIChatMessage } from '@/types/openai';
const isDev = process.env.NODE_ENV === 'development';
const OPENAI_PROXY_URL = process.env.OPENAI_PROXY_URL;
@ -10,13 +11,36 @@ const OPENAI_PROXY_URL = process.env.OPENAI_PROXY_URL;
*/
export interface OpenAIStreamPayload {
/**
* @title
* @title
* @default 0
*/
model: string;
frequency_penalty?: number;
/**
* @title
*/
max_tokens?: number;
/**
* @title
*/
messages: OpenAIChatMessage[];
/**
* @title
*/
model: string;
/**
* @title
*/
n?: number;
/**
* @title
* @default 0
*/
presence_penalty?: number;
/**
* @title
* @default true
*/
stream?: boolean;
/**
* @title
* @default 0.5
@ -27,29 +51,6 @@ export interface OpenAIStreamPayload {
* @default 1
*/
top_p?: number;
/**
* @title
* @default 0
*/
frequency_penalty?: number;
/**
* @title
* @default 0
*/
presence_penalty?: number;
/**
* @title
*/
max_tokens?: number;
/**
* @title
* @default true
*/
stream?: boolean;
/**
* @title
*/
n?: number;
}
export function OpenAIStream(payload: OpenAIStreamPayload) {
@ -59,13 +60,16 @@ export function OpenAIStream(payload: OpenAIStreamPayload) {
const chatMessages = messages.map((m) => {
switch (m.role) {
default:
case 'user':
case 'user': {
return new HumanChatMessage(m.content);
case 'system':
}
case 'system': {
return new SystemChatMessage(m.content);
}
case 'assistant':
case 'assistant': {
return new AIChatMessage(m.content);
}
}
});
@ -82,9 +86,7 @@ export function OpenAIStream(payload: OpenAIStreamPayload) {
{
streaming: true,
...params,
// 暂时设定不重试 ,后续看是否需要支持重试
maxRetries: 0,
verbose: true,
callbacks: [
{
handleLLMNewToken(token) {
@ -100,6 +102,9 @@ export function OpenAIStream(payload: OpenAIStreamPayload) {
},
},
],
// 暂时设定不重试 ,后续看是否需要支持重试
maxRetries: 0,
verbose: true,
},
isDev && OPENAI_PROXY_URL ? { basePath: OPENAI_PROXY_URL } : undefined,
);

View file

@ -24,8 +24,13 @@ export type ConfigCellProps = ConfigItem;
export const ConfigCell = memo<ConfigCellProps>(({ icon, label, value }) => {
const { styles } = useStyles();
return (
<Flexbox horizontal distribution={'space-between'} padding={'10px 12px'} className={styles.container}>
<Flexbox horizontal gap={8}>
<Flexbox
className={styles.container}
distribution={'space-between'}
horizontal
padding={'10px 12px'}
>
<Flexbox gap={8} horizontal>
<Icon icon={icon} />
<Flexbox>{label}</Flexbox>
</Flexbox>
@ -45,13 +50,13 @@ export const ConfigCellGroup = memo<CellGroupProps>(({ items }) => {
<Flexbox className={styles.container}>
{items.map(({ label, icon, value }, index) => (
<Flexbox
key={label}
horizontal
distribution={'space-between'}
padding={'10px 12px'}
className={items.length === index + 1 ? undefined : styles.split}
distribution={'space-between'}
horizontal
key={label}
padding={'10px 12px'}
>
<Flexbox horizontal gap={8}>
<Flexbox gap={8} horizontal>
<Icon icon={icon} />
<Flexbox>{label}</Flexbox>
</Flexbox>

View file

@ -1,4 +1,3 @@
import { agentSelectors, sessionSelectors, useChatStore } from '@/store/session';
import { Avatar } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import isEqual from 'fast-deep-equal';
@ -7,19 +6,21 @@ import { memo } from 'react';
import { Center, Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { agentSelectors, sessionSelectors, useChatStore } from '@/store/session';
import { ConfigCell, ConfigCellGroup } from './ConfigCell';
const useStyles = createStyles(({ css, token }) => ({
title: css`
font-size: ${token.fontSizeHeading4}px;
font-weight: bold;
`,
desc: css`
color: ${token.colorText};
`,
model: css`
color: ${token.colorTextTertiary};
`,
title: css`
font-size: ${token.fontSizeHeading4}px;
font-weight: bold;
`,
}));
const ReadMode = memo(() => {
@ -30,13 +31,13 @@ const ReadMode = memo(() => {
const model = useChatStore(agentSelectors.currentAgentModel, shallow);
return (
<Center style={{ marginTop: 8 }} gap={12} padding={'32px 16px'}>
<Avatar size={100} avatar={avatar} />
<Center gap={12} padding={'32px 16px'} style={{ marginTop: 8 }}>
<Avatar avatar={avatar} size={100} />
<Flexbox className={styles.title}>{title || '默认对话'}</Flexbox>
<Flexbox className={styles.model}>{model}</Flexbox>
<Flexbox className={styles.desc}>{session.meta.description}</Flexbox>
<Flexbox gap={12} flex={1} width={'100%'}>
<Flexbox flex={1} gap={12} width={'100%'}>
<ConfigCell icon={LucideBrain} label={'提示词'} />
<ConfigCellGroup

View file

@ -1,10 +1,11 @@
import { ActionIcon, DraggablePanel } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { LucideEdit, LucideX } from 'lucide-react';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { useChatStore } from '@/store/session';
import { ActionIcon, DraggablePanel } from '@lobehub/ui';
import { LucideEdit, LucideX } from 'lucide-react';
import ReadMode from './ReadMode';
const useStyles = createStyles(({ css, token }) => ({
@ -18,29 +19,38 @@ const useStyles = createStyles(({ css, token }) => ({
const Config = () => {
const { styles } = useStyles();
const [showAgentSettings, toggleConfig] = useChatStore((s) => [s.showAgentSettings, s.toggleConfig], shallow);
const [showAgentSettings, toggleConfig] = useChatStore(
(s) => [s.showAgentSettings, s.toggleConfig],
shallow,
);
return (
<DraggablePanel
className={styles.drawer}
expand={showAgentSettings}
expandable={false}
placement={'right'}
minWidth={400}
mode={'float'}
pin
placement={'right'}
resize={{ left: false }}
minWidth={400}
expand={showAgentSettings}
className={styles.drawer}
>
<Flexbox padding={16} horizontal align={'center'} distribution={'space-between'} className={styles.header}>
<Flexbox
align={'center'}
className={styles.header}
distribution={'space-between'}
horizontal
padding={16}
>
<Flexbox></Flexbox>
<Flexbox horizontal>
<ActionIcon icon={LucideEdit} title={'编辑'} />
<ActionIcon
icon={LucideX}
title={'关闭'}
onClick={() => {
toggleConfig(false);
}}
title={'关闭'}
/>
</Flexbox>
</Flexbox>

View file

@ -7,20 +7,25 @@ import { chatSelectors, useChatStore } from '@/store/session';
const List = () => {
const data = useChatStore(chatSelectors.currentChats, isEqual);
const [deleteMessage, resendMessage] = useChatStore((s) => [s.deleteMessage, s.resendMessage], shallow);
const [deleteMessage, resendMessage] = useChatStore(
(s) => [s.deleteMessage, s.resendMessage],
shallow,
);
return (
<ChatList
data={data}
onActionClick={(key, id) => {
switch (key) {
case 'delete':
case 'delete': {
deleteMessage(id);
break;
}
case 'regenerate':
case 'regenerate': {
resendMessage(id);
break;
}
}
}}
style={{ marginTop: 24 }}

View file

@ -16,7 +16,12 @@ const ChatInput = () => {
const [inputHeight] = useSettings((s) => [s.inputHeight], shallow);
const [totalToken, model, sendMessage, clearMessage] = useChatStore(
(s) => [chatSelectors.totalTokenCount(s), agentSelectors.currentAgentModel(s), s.sendMessage, s.clearMessage],
(s) => [
chatSelectors.totalTokenCount(s),
agentSelectors.currentAgentModel(s),
s.sendMessage,
s.clearMessage,
],
shallow,
);
@ -25,14 +30,14 @@ const ChatInput = () => {
expandable={false}
fullscreen={expand}
minHeight={200}
placement="bottom"
size={{ width: '100%', height: inputHeight }}
onSizeChange={(_, size) => {
if (!size) return;
useSettings.setState({
inputHeight: typeof size.height === 'string' ? Number.parseInt(size.height) : size.height,
});
}}
placement="bottom"
size={{ height: inputHeight, width: '100%' }}
>
<ChatInputArea
actions={

View file

@ -1,7 +1,7 @@
import { memo } from 'react';
import { createStyles } from 'antd-style';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import ChatList from './ChatList';
import ChatInput from './Input';

View file

@ -1,4 +1,3 @@
import { sessionSelectors, useChatStore } from '@/store/session';
import { ActionIcon, Avatar } from '@lobehub/ui';
import { createStyles, useTheme } from 'antd-style';
import { ArchiveIcon, MoreVerticalIcon, Share2Icon } from 'lucide-react';
@ -6,15 +5,17 @@ import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { sessionSelectors, useChatStore } from '@/store/session';
const useStyles = createStyles(({ css, token }) => ({
title: css`
font-weight: bold;
color: ${token.colorText};
`,
desc: css`
font-size: 12px;
color: ${token.colorTextTertiary};
`,
title: css`
font-weight: bold;
color: ${token.colorText};
`,
}));
const Header = memo(() => {
const theme = useTheme();
@ -38,9 +39,9 @@ const Header = memo(() => {
const { styles } = useStyles();
return (
<Flexbox
horizontal
align={'center'}
distribution={'space-between'}
horizontal
padding={8}
paddingInline={16}
style={{
@ -48,22 +49,22 @@ const Header = memo(() => {
gridArea: 'header',
}}
>
<Flexbox horizontal align={'center'} gap={12}>
<Avatar size={40} title={'123'} avatar={meta && sessionSelectors.getAgentAvatar(meta)} />
<Flexbox align={'center'} gap={12} horizontal>
<Avatar avatar={meta && sessionSelectors.getAgentAvatar(meta)} size={40} title={'123'} />
<Flexbox>
<Flexbox className={styles.title}>{meta?.title}</Flexbox>
<Flexbox className={styles.desc}>{meta?.description || '暂无描述'}</Flexbox>
</Flexbox>
</Flexbox>
<Flexbox horizontal gap={16}>
<Flexbox gap={16} horizontal>
<ActionIcon
title={'分享'}
icon={Share2Icon}
onClick={() => {
// genShareUrl();
}}
title={'分享'}
/>
<ActionIcon title={'归档'} icon={ArchiveIcon} />
<ActionIcon icon={ArchiveIcon} title={'归档'} />
<ActionIcon icon={MoreVerticalIcon} onClick={toggleConfig} />
</Flexbox>
</Flexbox>

View file

@ -1,12 +1,12 @@
import { ActionIcon, Logo, SearchBar } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { Plus } from 'lucide-react';
import Link from 'next/link';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { useChatStore } from '@/store/session';
import Link from 'next/link';
export const useStyles = createStyles(({ css, token }) => ({
logo: css`
@ -21,22 +21,25 @@ export const useStyles = createStyles(({ css, token }) => ({
const Header = memo(() => {
const { styles } = useStyles();
const [keywords, createSession] = useChatStore((s) => [s.searchKeywords, s.createSession], shallow);
const [keywords, createSession] = useChatStore(
(s) => [s.searchKeywords, s.createSession],
shallow,
);
return (
<Flexbox gap={16} padding={'16px 12px 0 16px'} className={styles.top}>
<Flexbox horizontal distribution={'space-between'}>
<Flexbox className={styles.top} gap={16} padding={'16px 12px 0 16px'}>
<Flexbox distribution={'space-between'} horizontal>
<Link href={'/'}>
<Logo type={'text'} size={36} className={styles.logo} />
<Logo className={styles.logo} size={36} type={'text'} />
</Link>
<ActionIcon title={'新对话'} icon={Plus} style={{ minWidth: 32 }} onClick={createSession} />
<ActionIcon icon={Plus} onClick={createSession} style={{ minWidth: 32 }} title={'新对话'} />
</Flexbox>
<SearchBar
allowClear
value={keywords}
onChange={(e) => useChatStore.setState({ searchKeywords: e.target.value })}
placeholder="Search..."
type={'block'}
onChange={(e) => useChatStore.setState({ searchKeywords: e.target.value })}
value={keywords}
/>
</Flexbox>
);

View file

@ -1,12 +1,12 @@
import { CloseOutlined } from '@ant-design/icons';
import { Avatar, List } from '@lobehub/ui';
import { Popconfirm } from 'antd';
import { createStyles } from 'antd-style';
import { FC, memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { sessionSelectors, useChatStore } from '@/store/session';
import { CloseOutlined } from '@ant-design/icons';
import { Popconfirm } from 'antd';
const useStyles = createStyles(({ css, cx }) => {
const closeCtn = css`
@ -23,6 +23,10 @@ const useStyles = createStyles(({ css, cx }) => {
opacity: 0;
`;
return {
active: css`
opacity: 1;
`,
closeCtn,
container: css`
position: relative;
@ -35,10 +39,6 @@ const useStyles = createStyles(({ css, cx }) => {
time: css`
align-self: flex-start;
`,
closeCtn,
active: css`
opacity: 1;
`,
};
});
@ -51,43 +51,51 @@ interface SessionItemProps {
const SessionItem: FC<SessionItemProps> = memo(({ id, active, simple = true, loading }) => {
const { styles, theme, cx } = useStyles();
const [title, systemRole, avatar, avatarBackground, updateAt, switchAgent, removeSession] = useChatStore((s) => {
const session = sessionSelectors.getSessionById(id)(s);
const meta = session.meta;
const [title, systemRole, avatar, avatarBackground, updateAt, switchAgent, removeSession] =
useChatStore((s) => {
const session = sessionSelectors.getSessionById(id)(s);
const meta = session.meta;
const systemRole = session.config.systemRole;
const systemRole = session.config.systemRole;
return [
meta.title || systemRole || '默认角色',
systemRole,
sessionSelectors.getAgentAvatar(meta),
meta.backgroundColor,
session?.updateAt,
s.switchSession,
s.removeSession,
];
}, shallow);
return [
meta.title || systemRole || '默认角色',
systemRole,
sessionSelectors.getAgentAvatar(meta),
meta.backgroundColor,
session?.updateAt,
s.switchSession,
s.removeSession,
];
}, shallow);
return (
<Flexbox gap={4} paddingBlock={4} className={styles.container}>
<Flexbox className={styles.container} gap={4} paddingBlock={4}>
<Popconfirm
title={'即将删除该会话,删除后该将无法找回,请确认你的操作。'}
placement={'right'}
arrow={false}
overlayStyle={{ width: 280 }}
okButtonProps={{ danger: true }}
onConfirm={() => removeSession(id)}
overlayStyle={{ width: 280 }}
placement={'right'}
title={'即将删除该会话,删除后该将无法找回,请确认你的操作。'}
>
<CloseOutlined className={cx(styles.closeCtn, active && styles.active)} />
</Popconfirm>
<List.Item
loading={loading}
title={title}
description={simple ? undefined : systemRole}
active={active}
date={updateAt}
avatar={
<Avatar
avatar={avatar}
background={avatarBackground}
shape="circle"
size={46}
title={title}
/>
}
classNames={{ time: styles.time }}
avatar={<Avatar avatar={avatar} size={46} shape="circle" title={title} background={avatarBackground} />}
date={updateAt}
description={simple ? undefined : systemRole}
loading={loading}
onClick={() => {
switchAgent(id);
}}
@ -95,6 +103,7 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active, simple = true, loa
alignItems: 'center',
color: theme.colorText,
}}
title={title}
/>
</Flexbox>
);

View file

@ -19,7 +19,7 @@ export const useStyles = createStyles(({ css, token }) => ({
justify-content: center;
margin-top: 8px;
padding: 12px 12px;
padding: 12px;
background: ${rgba(token.colorBgLayout, 0.5)};
backdrop-filter: blur(8px);
@ -35,7 +35,13 @@ const SessionList = memo(() => {
return (
<>
{list.map(({ id }) => (
<SessionItem key={id} active={activeId === id} id={id} simple={false} loading={loading && id === activeId} />
<SessionItem
active={activeId === id}
id={id}
key={id}
loading={loading && id === activeId}
simple={false}
/>
))}
</>
);

View file

@ -3,6 +3,7 @@ import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import FolderPanel from '@/features/FolderPanel';
import Header from './Header';
import SessionList from './List';

View file

@ -1,22 +1,33 @@
import { useSettings } from '@/store/settings';
import { ActionIcon, Logo, SideNav } from '@lobehub/ui';
import { Album, MessageSquare, Settings2 } from 'lucide-react';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import { useSettings } from '@/store/settings';
const Sidebar = memo(() => {
const [tab, setTab] = useSettings((s) => [s.sidebarKey, s.switchSideBar], shallow);
return (
<SideNav
avatar={<Logo size={40} />}
bottomActions={<ActionIcon icon={Settings2} />}
style={{ height: '100vh' }}
topActions={
<>
<ActionIcon icon={MessageSquare} size="large" active={tab === 'chat'} onClick={() => setTab('chat')} />
<ActionIcon icon={Album} size="large" active={tab === 'market'} onClick={() => setTab('market')} />
<ActionIcon
active={tab === 'chat'}
icon={MessageSquare}
onClick={() => setTab('chat')}
size="large"
/>
<ActionIcon
active={tab === 'market'}
icon={Album}
onClick={() => setTab('market')}
size="large"
/>
</>
}
bottomActions={<ActionIcon icon={Settings2} />}
/>
);
});

View file

@ -38,12 +38,15 @@ const ChatLayout = () => {
<Head>
<title>{title ? `${title} - LobeChat` : 'LobeChat'}</title>
</Head>
<Flexbox width={'100%'} horizontal>
<Flexbox horizontal width={'100%'}>
<Sidebar />
<Sessions />
<Flexbox flex={1}>
<Header />
<Flexbox id={'lobe-conversion-container'} style={{ position: 'relative', height: 'calc(100vh - 64px)' }}>
<Flexbox
id={'lobe-conversion-container'}
style={{ height: 'calc(100vh - 64px)', position: 'relative' }}
>
<Conversation />
<Config />
</Flexbox>

View file

@ -1,5 +1 @@
export {default} from './[id].page';
export { default } from './[id].page';

View file

@ -1,5 +1 @@
export {default} from './chat/index.page';
export { default } from './chat/index.page';

View file

@ -4,30 +4,30 @@ import { OpenAIStreamPayload } from '@/pages/api/OpenAIStream';
export const promptSummaryAgentName = (content: string): Partial<OpenAIStreamPayload> => ({
messages: [
{
role: 'system',
content: `你是一名擅长起名的起名大师,你需要将用户的描述总结为 20 个字以内的角色,格式要求如下:
: {JSON引用字符串}
: {}
`,
role: 'system',
},
{
role: 'user',
content: `输入: {你是一名专业的前端开发者,擅长结合 vitest 和\`testing-library/react\` 书写单元测试。接下来用户会输入一串 ts 代码,你需要给出完善的单元测试。\n你需要注意单元测试代码中不应该使用 jest 。如果需要使用 \`jest.fn\`,请使用 \`vi.fn\` 替换}`,
},
{ role: 'assistant', content: '前端 vitest 测试专家' },
{
role: 'user',
},
{ content: '前端 vitest 测试专家', role: 'assistant' },
{
content: `输入: {你是一名前端专家,请将下面的代码转成 ts不要修改实现。如果原本 js 中没有定义的全局变量,需要补充 declare 的类型声明。}`,
},
{ role: 'assistant', content: 'js 转 ts 专家' },
{
role: 'user',
},
{ content: 'js 转 ts 专家', role: 'assistant' },
{
content: `输入:{你是一名擅长比喻和隐喻的UX Writter。用户会输入文案你需要给出3个优化后的结果使用 markdown格式的文本。下面是一个例子
}`,
role: 'user',
},
{ role: 'assistant', content: '文案比喻优化专家' },
{ role: 'user', content: `输入: {${content}}` },
{ content: '文案比喻优化专家', role: 'assistant' },
{ content: `输入: {${content}}`, role: 'user' },
],
});
@ -35,7 +35,6 @@ export const promptSummaryAgentName = (content: string): Partial<OpenAIStreamPay
export const promptPickEmoji = (content: string): Partial<OpenAIStreamPayload> => ({
messages: [
{
role: 'system',
content: `你是一名非常懂设计与时尚的设计师,你需要从用户的描述中匹配一个合适的 emoji。
输入:你是一名精通体验设计的设计系统设计师 token token
: 💅
@ -43,10 +42,11 @@ export const promptPickEmoji = (content: string): Partial<OpenAIStreamPayload> =
输入:用户会输入一串 ts 100%
: 🧪
`,
role: 'system',
},
{
role: 'user',
content: `输入:${content}`,
role: 'user',
},
],
});

View file

@ -1,29 +1,37 @@
import { OpenAIStreamPayload } from '@/pages/api/OpenAIStream';
import { OpenAIChatMessage } from '@/types/openai';
export const promptSummaryTitle = (messages: OpenAIChatMessage[]): Partial<OpenAIStreamPayload> => ({
export const promptSummaryTitle = (
messages: OpenAIChatMessage[],
): Partial<OpenAIStreamPayload> => ({
messages: [
{
content:
'你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不需要包含标点符号',
role: 'system',
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不需要包含标点符号',
},
{
role: 'user',
content: `${messages.map((message) => `${message.role}: ${message.content}`).join('\n')}
10`,
role: 'user',
},
],
});
export const promptSummaryDescription = (messages: OpenAIChatMessage[]): Partial<OpenAIStreamPayload> => ({
export const promptSummaryDescription = (
messages: OpenAIChatMessage[],
): Partial<OpenAIStreamPayload> => ({
messages: [
{ role: 'system', content: '你是一名擅长会话的助理你需要将用户的会话做一个3句话以内的总结。' },
{
role: 'user',
content: '你是一名擅长会话的助理你需要将用户的会话做一个3句话以内的总结。',
role: 'system',
},
{
content: `${messages.map((message) => `${message.role}: ${message.content}`).join('\n')}
50 `,
role: 'user',
},
],
});

View file

@ -1,6 +1,7 @@
import type { OpenAIStreamPayload } from '@/pages/api/OpenAIStream';
import { merge } from 'lodash-es';
import type { OpenAIStreamPayload } from '@/pages/api/OpenAIStream';
import { URLS } from './url';
/**
@ -13,18 +14,18 @@ export const fetchChatModel = (
const payload = merge(
{
model: 'gpt-3.5-turbo',
temperature: 0.6,
stream: true,
temperature: 0.6,
},
params,
);
return fetch(URLS.openai, {
method: 'POST',
body: JSON.stringify(payload),
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
method: 'POST',
signal,
});
};

View file

@ -8,11 +8,11 @@ import { fetchAIFactory } from '@/utils/fetch';
export const fetchLangChain = fetchAIFactory(
(params: LangChainParams, signal?: AbortSignal | undefined) =>
fetch(URLS.chain, {
method: 'POST',
body: JSON.stringify(params),
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
method: 'POST',
signal,
}),
);

View file

@ -3,6 +3,6 @@ const isDev = process.env.NODE_ENV === 'development';
const prefix = isDev ? '-dev' : '';
export const URLS = {
openai: '/api/openai' + prefix,
chain: '/api/chain' + prefix,
openai: '/api/openai' + prefix,
};

View file

@ -1,6 +1,6 @@
import { SessionStore } from '@/store/session';
import { LanguageModel } from '@/types/llm';
import { sessionSelectors } from '../session';
const currentAgentTitle = (s: SessionStore) => {
@ -31,8 +31,8 @@ const currentAgentModel = (s: SessionStore): LanguageModel => {
};
export const agentSelectors = {
currentAgentConfig,
currentAgentAvatar,
currentAgentConfig,
currentAgentModel,
currentAgentTitle,
};

View file

@ -12,6 +12,8 @@ const LOADING_FLAT = '...';
export interface ChatAction {
clearMessage: () => void;
deleteMessage: (id: string) => void;
/**
* @title
* @param payload -
@ -20,36 +22,40 @@ export interface ChatAction {
dispatchMessage: (payload: MessageDispatch) => void;
generateMessage: (messages: ChatMessage[], options: FetchSSEOptions) => Promise<void>;
/**
* @title
* @param index -
* @returns void
*/
handleMessageEditing: (messageId: string | undefined) => void;
/**
* @title
* @param index -
* @returns Promise<void>
*/
resendMessage: (id: string) => Promise<void>;
/**
* @title
* @returns Promise<void>
*/
sendMessage: (text: string) => Promise<void>;
deleteMessage: (id: string) => void;
}
export const createChatSlice: StateCreator<SessionStore, [['zustand/devtools', never]], [], ChatAction> = (
set,
get,
) => ({
export const createChatSlice: StateCreator<
SessionStore,
[['zustand/devtools', never]],
[],
ChatAction
> = (set, get) => ({
clearMessage: () => {
get().dispatchMessage({ type: 'resetMessages' });
},
deleteMessage: (id) => {
get().dispatchMessage({ id, type: 'deleteMessage' });
},
dispatchMessage: (payload) => {
const { activeId } = get();
const session = sessionSelectors.currentSession(get());
@ -183,8 +189,4 @@ export const createChatSlice: StateCreator<SessionStore, [['zustand/devtools', n
},
});
},
deleteMessage: (id) => {
get().dispatchMessage({ type: 'deleteMessage', id });
},
});

View file

@ -1,6 +1,6 @@
export interface ChatState {
editingMessageId?: string;
chatLoading: boolean;
editingMessageId?: string;
}
export const initialChatState: ChatState = {

View file

@ -6,18 +6,18 @@ import { MetaData } from '@/types/meta';
import { nanoid } from '@/utils/uuid';
interface AddMessage {
type: 'addMessage';
message: string;
role: LLMRoleType;
id?: string;
quotaId?: string;
parentId?: string;
message: string;
meta?: MetaData;
parentId?: string;
quotaId?: string;
role: LLMRoleType;
type: 'addMessage';
}
interface DeleteMessage {
type: 'deleteMessage';
id: string;
type: 'deleteMessage';
}
interface ResetMessages {
@ -25,38 +25,43 @@ interface ResetMessages {
}
interface UpdateMessage {
type: 'updateMessage';
id: string;
key: keyof ChatMessage;
type: 'updateMessage';
value: ChatMessage[keyof ChatMessage];
}
export type MessageDispatch = AddMessage | DeleteMessage | ResetMessages | UpdateMessage;
export const messagesReducer = (state: ChatMessageMap, payload: MessageDispatch): ChatMessageMap => {
export const messagesReducer = (
state: ChatMessageMap,
payload: MessageDispatch,
): ChatMessageMap => {
switch (payload.type) {
case 'addMessage':
case 'addMessage': {
return produce(state, (draftState) => {
const mid = payload.id || nanoid();
draftState[mid] = {
id: mid,
role: payload.role,
content: payload.message,
meta: payload.meta || {},
quotaId: payload.quotaId,
parentId: payload.parentId,
updateAt: Date.now(),
createAt: Date.now(),
id: mid,
meta: payload.meta || {},
parentId: payload.parentId,
quotaId: payload.quotaId,
role: payload.role,
updateAt: Date.now(),
};
});
}
case 'deleteMessage':
case 'deleteMessage': {
return produce(state, (draftState) => {
delete draftState[payload.id];
});
}
case 'updateMessage':
case 'updateMessage': {
return produce(state, (draftState) => {
const { id, key, value } = payload;
const message = draftState[id];
@ -66,11 +71,14 @@ export const messagesReducer = (state: ChatMessageMap, payload: MessageDispatch)
message[key] = value;
message.updateAt = Date.now();
});
}
case 'resetMessages':
case 'resetMessages': {
return {};
}
default:
default: {
throw new Error('暂未实现的 type请检查 reducer');
}
}
};

View file

@ -1,6 +1,7 @@
import { ChatMessage } from '@/types/chatMessage';
import { encode } from 'gpt-tokenizer';
import { ChatMessage } from '@/types/chatMessage';
import type { SessionStore } from '../../store';
import { sessionSelectors } from '../session';
@ -40,8 +41,8 @@ const systemRoleTokenCount = (s: SessionStore) => systemRoleTokens(s).length;
export const chatSelectors = {
currentChats: currentChatsSel,
systemRole: systemRoleSel,
totalTokens,
totalTokenCount,
systemRoleTokens,
systemRoleTokenCount,
systemRoleTokens,
totalTokenCount,
totalTokens,
};

View file

@ -15,6 +15,12 @@ export interface SessionAction {
* @returns void
*/
createSession: () => Promise<void>;
/**
*
* @param payload -
*/
dispatchSession: (payload: SessionDispatch) => void;
/**
* @title
* @param index -
@ -29,12 +35,6 @@ export interface SessionAction {
*/
switchSession: (sessionId?: string | 'new') => void;
/**
*
* @param payload -
*/
dispatchSession: (payload: SessionDispatch) => void;
/**
*
* @returns
@ -42,48 +42,50 @@ export interface SessionAction {
// genShareUrl: () => string;
}
export const createSessionSlice: StateCreator<SessionStore, [['zustand/devtools', never]], [], SessionAction> = (
set,
get,
) => ({
dispatchSession: (payload) => {
const { type, ...res } = payload;
set({ sessions: sessionsReducer(get().sessions, payload) }, false, {
type: `dispatchChat/${type}`,
payload: res,
});
},
export const createSessionSlice: StateCreator<
SessionStore,
[['zustand/devtools', never]],
[],
SessionAction
> = (set, get) => ({
createSession: async () => {
const { dispatchSession, switchSession } = get();
const timestamp = Date.now();
const newSession: LobeAgentSession = {
id: uuid(),
createAt: timestamp,
updateAt: timestamp,
type: LobeSessionType.Agent,
chats: {},
meta: {
title: '默认对话',
},
config: {
model: LanguageModel.GPT3_5,
systemRole: '',
params: {
temperature: 0.6,
},
systemRole: '',
},
createAt: timestamp,
id: uuid(),
meta: {
title: '默认对话',
},
type: LobeSessionType.Agent,
updateAt: timestamp,
};
dispatchSession({ type: 'addSession', session: newSession });
dispatchSession({ session: newSession, type: 'addSession' });
switchSession(newSession.id);
},
dispatchSession: (payload) => {
const { type, ...res } = payload;
set({ sessions: sessionsReducer(get().sessions, payload) }, false, {
payload: res,
type: `dispatchChat/${type}`,
});
},
removeSession: (sessionId) => {
get().dispatchSession({ type: 'removeSession', id: sessionId });
get().dispatchSession({ id: sessionId, type: 'removeSession' });
Router.push('/');
},

View file

@ -1,52 +1,53 @@
import { produce } from 'immer';
import { ChatMessageMap } from '@/types/chatMessage';
import { MetaData } from '@/types/meta';
import { LobeAgentConfig, LobeAgentSession, LobeSessions } from '@/types/session';
import { produce } from 'immer';
/**
* @title
*/
interface AddSession {
/**
* @param session -
*/
session: LobeAgentSession;
/**
* @param type -
* @default 'addChat'
*/
type: 'addSession';
/**
* @param session -
*/
session: LobeAgentSession;
}
interface RemoveSession {
type: 'removeSession';
id: string;
type: 'removeSession';
}
/**
* @title
*/
interface UpdateSessionChat {
type: 'updateSessionChat';
chats: ChatMessageMap;
/**
* ID
*/
id: string;
chats: ChatMessageMap;
type: 'updateSessionChat';
}
interface UpdateSessionMeta {
type: 'updateSessionMeta';
id: string;
key: keyof MetaData;
type: 'updateSessionMeta';
value: any;
}
interface UpdateSessionAgentConfig {
type: 'updateSessionConfig';
id: string;
config: Partial<LobeAgentConfig>;
id: string;
type: 'updateSessionConfig';
}
export type SessionDispatch =
@ -58,17 +59,19 @@ export type SessionDispatch =
export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch): LobeSessions => {
switch (payload.type) {
case 'addSession':
case 'addSession': {
return produce(state, (draft) => {
draft[payload.session.id] = payload.session;
});
}
case 'removeSession':
case 'removeSession': {
return produce(state, (draft) => {
delete draft[payload.id];
});
}
case 'updateSessionMeta':
case 'updateSessionMeta': {
return produce(state, (draft) => {
const chat = draft[payload.id];
if (!chat) return;
@ -77,16 +80,18 @@ export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch):
chat.meta[key] = value;
});
}
case 'updateSessionChat':
case 'updateSessionChat': {
return produce(state, (draft) => {
const chat = draft[payload.id];
if (!chat) return;
chat.chats = payload.chats;
});
}
case 'updateSessionConfig':
case 'updateSessionConfig': {
return produce(state, (draft) => {
const { id, config } = payload;
const chat = draft[id];
@ -94,8 +99,10 @@ export const sessionsReducer = (state: LobeSessions, payload: SessionDispatch):
chat.config = { ...chat.config, ...config };
});
}
default:
default: {
return produce(state, () => {});
}
}
};

View file

@ -1,12 +1,20 @@
import { getAgentAvatar } from './chat';
import { chatListSel, currentSessionSafe, currentSessionSel, getSessionById, getSessionMetaById } from './list';
import {
chatListSel,
currentSessionSafe,
currentSessionSel,
getSessionById,
getSessionMetaById,
} from './list';
export const sessionSelectors = {
chatList: chatListSel,
currentSession: currentSessionSel,
currentSessionSafe,
chatList: chatListSel,
getAgentAvatar,
getSessionById,
// sessionTree: sessionTreeSel,
getSessionMetaById,
getSessionById,
getAgentAvatar,
};

View file

@ -1,7 +1,6 @@
import { SessionStore } from '@/store/session';
import { LobeAgentSession } from '@/types/session';
import { MetaData } from '@/types/meta';
import { LobeAgentSession } from '@/types/session';
import { filterWithKeywords } from '@/utils/filter';
import { initLobeSession } from '../initialState';

View file

@ -7,26 +7,26 @@ import { ConfigSettings } from '@/types/exportConfig';
export type SidebarTabKey = 'chat' | 'market';
interface SettingsStore {
sessionsWidth: number;
inputHeight: number;
avatar?: string;
sessionExpandable?: boolean;
sidebarKey: SidebarTabKey;
themeMode?: ThemeMode;
importSettings: (settings: ConfigSettings) => void;
inputHeight: number;
sessionExpandable?: boolean;
sessionsWidth: number;
sidebarKey: SidebarTabKey;
switchSideBar: (key: SidebarTabKey) => void;
themeMode?: ThemeMode;
}
export const useSettings = create<SettingsStore>()(
persist<SettingsStore>(
(set) => ({
sessionsWidth: 320,
inputHeight: 200,
sessionExpandable: true,
sidebarKey: 'chat',
importSettings: (settings) => {
set({ ...settings });
},
inputHeight: 200,
sessionExpandable: true,
sessionsWidth: 320,
sidebarKey: 'chat',
switchSideBar: (key) => {
set({ sidebarKey: key });
},

View file

@ -14,11 +14,7 @@ export interface ChatMessageError {
}
export interface ChatMessage extends BaseDataModel {
/**
*
* @description
*/
role: LLMRoleType;
archive?: boolean;
/**
* @title
@ -27,20 +23,24 @@ export interface ChatMessage extends BaseDataModel {
content: string;
error?: any;
archive?: boolean;
parentId?: string;
// 引用
quotaId?: string;
// 扩展字段
extra?: {
// 翻译
translate: {
to: string;
target: string;
to: string;
};
// 语音
} & Record<string, any>;
parentId?: string;
// 引用
quotaId?: string;
/**
*
* @description
*/
role: LLMRoleType;
}
export type ChatMessageMap = Record<string, ChatMessage>;

View file

@ -2,7 +2,19 @@ import { ChatMessage } from '@lobehub/ui';
export interface LangChainParams {
llm: {
/**
*
*/
frequency_penalty?: number;
/**
*
*/
max_tokens?: number;
model: string;
/**
*
*/
presence_penalty?: number;
/**
*
* @default 0.6
@ -12,18 +24,6 @@ export interface LangChainParams {
*
*/
top_p?: number;
/**
*
*/
frequency_penalty?: number;
/**
*
*/
presence_penalty?: number;
/**
*
*/
max_tokens?: number;
};
/**

View file

@ -1,11 +1,4 @@
export interface MetaData {
/**
*
* @description 使
*/
title?: string;
description?: string;
tag?: string[];
/**
*
* @description 使
@ -16,11 +9,18 @@ export interface MetaData {
* @description 使
*/
backgroundColor?: string;
description?: string;
tag?: string[];
/**
*
* @description 使
*/
title?: string;
}
export interface BaseDataModel {
createAt: number;
id: string;
meta: MetaData;
updateAt: number;
createAt: number;
}

View file

@ -14,18 +14,22 @@ export enum LobeSessionType {
}
interface LobeSessionBase extends BaseDataModel {
/**
*
*/
type: LobeSessionType;
/**
*
*/
chats: ChatMessageMap;
/**
*
*/
type: LobeSessionType;
}
export interface LobeAgentConfig {
/**
*
*/
example?: LLMExample;
/**
* 使
* @default gpt-3.5-turbo
@ -39,21 +43,17 @@ export interface LobeAgentConfig {
*
*/
systemRole: string;
/**
*
*/
example?: LLMExample;
}
/**
* Lobe Agent会话
*/
export interface LobeAgentSession extends LobeSessionBase {
type: LobeSessionType.Agent;
/**
*
*/
config: LobeAgentConfig;
type: LobeSessionType.Agent;
}
export type LobeSessions = Record<string, LobeAgentSession>;

View file

@ -3,16 +3,16 @@
* @template T -
*/
export interface Migration<T = any> {
/**
*
*/
version: number;
/**
*
* @param data -
* @returns
*/
migrate(data: MigrationData<T>): MigrationData;
/**
*
*/
version: number;
}
/**

View file

@ -8,8 +8,8 @@ export class StrCompressor {
* @ignore
*/
private instance!: {
decompress(buf: Uint8Array): Uint8Array;
compress(buf: Uint8Array, options?: any): Uint8Array;
decompress(buf: Uint8Array): Uint8Array;
};
async init(): Promise<void> {

View file

@ -21,8 +21,8 @@ const codeMessage: Record<number, string> = {
};
export interface FetchSSEOptions {
onMessageHandle?: (text: string) => void;
onErrorHandle?: (error: ChatMessageError) => void;
onMessageHandle?: (text: string) => void;
}
/**
@ -68,15 +68,7 @@ export const fetchSSE = async (fetchFn: () => Promise<Response>, options: FetchS
};
interface FetchAITaskResultParams<T> {
/**
*
*/
params: T;
/**
*
* @param text -
*/
onMessageHandle?: (text: string) => void;
abortController?: AbortController;
/**
*
*/
@ -86,13 +78,27 @@ interface FetchAITaskResultParams<T> {
* @param loading -
*/
onLoadingChange?: (loading: boolean) => void;
/**
*
* @param text -
*/
onMessageHandle?: (text: string) => void;
abortController?: AbortController;
/**
*
*/
params: T;
}
export const fetchAIFactory =
<T>(fetcher: (params: T, signal?: AbortSignal) => Promise<Response>) =>
async ({ params, onMessageHandle, onError, onLoadingChange, abortController }: FetchAITaskResultParams<T>) => {
async ({
params,
onMessageHandle,
onError,
onLoadingChange,
abortController,
}: FetchAITaskResultParams<T>) => {
const errorHandle = (error: Error) => {
onLoadingChange?.(false);
if (abortController?.signal.aborted) {
@ -112,10 +118,10 @@ export const fetchAIFactory =
onLoadingChange?.(true);
const data = await fetchSSE(() => fetcher(params, abortController?.signal), {
onMessageHandle,
onErrorHandle: (error) => {
errorHandle(new Error(error.message));
},
onMessageHandle,
}).catch(errorHandle);
onLoadingChange?.(false);

View file

@ -1,8 +1,4 @@
// generate('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 16); //=> "4f90d13a42"
import { customAlphabet } from 'nanoid/non-secure';
export const nanoid = customAlphabet(
@ -10,4 +6,4 @@ export const nanoid = customAlphabet(
8,
);
export {v4 as uuid} from 'uuid';
export { v4 as uuid } from 'uuid';

View file

@ -20,6 +20,6 @@
"@/*": ["src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}