diff --git a/src/renderer/src/assets/styles/markdown.scss b/src/renderer/src/assets/styles/markdown.scss index ecb875f6..ec3dc9b1 100644 --- a/src/renderer/src/assets/styles/markdown.scss +++ b/src/renderer/src/assets/styles/markdown.scss @@ -1,6 +1,6 @@ .markdown { color: #fff; - font-size: 14px; + font-size: 15px; line-height: 1.6; user-select: text; @@ -89,6 +89,8 @@ font-weight: 600; padding: 3px 5px; border-radius: 2px; + font-size: 90%; + display: inline-block; font-family: ui-monospace, SFMono-Regular, @@ -97,7 +99,5 @@ Consolas, Liberation Mono, monospace; - font-size: 80%; - display: inline-block; } } diff --git a/src/renderer/src/config/constant.ts b/src/renderer/src/config/constant.ts new file mode 100644 index 00000000..d1db0dff --- /dev/null +++ b/src/renderer/src/config/constant.ts @@ -0,0 +1 @@ +export const DEFAULT_TOPIC_NAME = 'Default Topic' diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts new file mode 100644 index 00000000..686c0027 --- /dev/null +++ b/src/renderer/src/hooks/useTopic.ts @@ -0,0 +1,12 @@ +import { Agent } from '@renderer/types' +import { useEffect, useState } from 'react' + +export function useActiveTopic(agent: Agent) { + const [activeTopic, setActiveTopic] = useState(agent?.topics[0]) + + useEffect(() => { + agent?.topics && setActiveTopic(agent?.topics[0]) + }, [agent]) + + return { activeTopic, setActiveTopic } +} diff --git a/src/renderer/src/pages/home/components/Chat/Chat.tsx b/src/renderer/src/pages/home/components/Chat/Chat.tsx index cfe574f6..1a05664a 100644 --- a/src/renderer/src/pages/home/components/Chat/Chat.tsx +++ b/src/renderer/src/pages/home/components/Chat/Chat.tsx @@ -1,11 +1,12 @@ import { Agent } from '@renderer/types' -import { FC, useEffect, useState } from 'react' +import { FC } from 'react' import styled from 'styled-components' import Inputbar from './Inputbar' import Conversations from './Conversations' import { Flex } from 'antd' import TopicList from './TopicList' import { useAgent } from '@renderer/hooks/useAgents' +import { useActiveTopic } from '@renderer/hooks/useTopic' interface Props { agent: Agent @@ -13,11 +14,7 @@ interface Props { const Chat: FC = (props) => { const { agent } = useAgent(props.agent.id) - const [activeTopic, setActiveTopic] = useState(agent.topics[0]) - - useEffect(() => { - setActiveTopic(agent.topics[0]) - }, [agent]) + const { activeTopic, setActiveTopic } = useActiveTopic(agent) if (!agent) { return null diff --git a/src/renderer/src/pages/home/components/Chat/Conversations.tsx b/src/renderer/src/pages/home/components/Chat/Conversations.tsx index f4935bb6..0ce5b786 100644 --- a/src/renderer/src/pages/home/components/Chat/Conversations.tsx +++ b/src/renderer/src/pages/home/components/Chat/Conversations.tsx @@ -1,13 +1,16 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' -import { openaiProvider } from '@renderer/services/provider' import { Agent, Message, Topic } from '@renderer/types' -import { runAsyncFunction, uuid } from '@renderer/utils' import localforage from 'localforage' import { FC, useCallback, useEffect, useState } from 'react' import styled from 'styled-components' import MessageItem from './Message' import { reverse } from 'lodash' import hljs from 'highlight.js' +import { fetchChatCompletion, fetchConversationSummary } from '@renderer/services/api' +import { getTopicMessages } from '@renderer/services/topic' +import { useAgent } from '@renderer/hooks/useAgents' +import { DEFAULT_TOPIC_NAME } from '@renderer/config/constant' +import { runAsyncFunction } from '@renderer/utils' interface Props { agent: Agent @@ -17,6 +20,7 @@ interface Props { const Conversations: FC = ({ agent, topic }) => { const [messages, setMessages] = useState([]) const [lastMessage, setLastMessage] = useState(null) + const { updateTopic } = useAgent(agent.id) const onSendMessage = useCallback( (message: Message) => { @@ -30,59 +34,37 @@ const Conversations: FC = ({ agent, topic }) => { [messages, topic] ) - const fetchChatCompletion = useCallback( - async (message: Message) => { - const stream = await openaiProvider.chat.completions.create({ - model: 'Qwen/Qwen2-7B-Instruct', - messages: [{ role: 'user', content: message.content }], - stream: true - }) - - const _message: Message = { - id: uuid(), - role: 'agent', - content: '', - agentId: agent.id, - topicId: topic.id, - createdAt: 'now' + const autoRenameTopic = useCallback(async () => { + if (topic.name === DEFAULT_TOPIC_NAME && messages.length >= 2) { + const summaryText = await fetchConversationSummary({ messages }) + if (summaryText) { + updateTopic({ ...topic, name: summaryText }) } - - let content = '' - - for await (const chunk of stream) { - content = content + (chunk.choices[0]?.delta?.content || '') - setLastMessage({ ..._message, content }) - } - - _message.content = content - - EventEmitter.emit(EVENT_NAMES.AI_CHAT_COMPLETION, _message) - - return _message - }, - [agent.id, topic] - ) + } + }, [messages, topic, updateTopic]) useEffect(() => { const unsubscribes = [ EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => { onSendMessage(msg) - fetchChatCompletion(msg) + fetchChatCompletion({ agent, message: msg, topic, onResponse: setLastMessage }) }), EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => { setLastMessage(null) onSendMessage(msg) - }) + setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100) + }), + EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic) ] return () => unsubscribes.forEach((unsub) => unsub()) - }, [fetchChatCompletion, onSendMessage]) + }, [agent, autoRenameTopic, onSendMessage, topic]) useEffect(() => { runAsyncFunction(async () => { - const _topic = await localforage.getItem(`topic:${topic.id}`) - setMessages(_topic ? _topic.messages : []) + const messages = await getTopicMessages(topic.id) + setMessages(messages) }) - }, [topic]) + }, [topic.id]) useEffect(() => hljs.highlightAll()) diff --git a/src/renderer/src/pages/home/components/Chat/TopicList.tsx b/src/renderer/src/pages/home/components/Chat/TopicList.tsx index 1ede5f72..9f0dcc3a 100644 --- a/src/renderer/src/pages/home/components/Chat/TopicList.tsx +++ b/src/renderer/src/pages/home/components/Chat/TopicList.tsx @@ -1,6 +1,8 @@ import PromptPopup from '@renderer/components/Popups/PromptPopup' import { useAgent } from '@renderer/hooks/useAgents' import { useShowRightSidebar } from '@renderer/hooks/useStore' +import { fetchConversationSummary } from '@renderer/services/api' +import { getTopicMessages } from '@renderer/services/topic' import { Agent, Topic } from '@renderer/types' import { Dropdown, MenuProps } from 'antd' import { FC, useRef } from 'react' @@ -14,13 +16,22 @@ interface Props { const TopicList: FC = ({ agent, activeTopic, setActiveTopic }) => { const { showRightSidebar } = useShowRightSidebar() - const currentTopic = useRef(null) const { removeTopic, updateTopic } = useAgent(agent.id) + const currentTopic = useRef(null) const items: MenuProps['items'] = [ { label: 'AI Rename', - key: 'ai-rename' + key: 'ai-rename', + async onClick() { + if (currentTopic.current) { + const messages = await getTopicMessages(currentTopic.current.id) + const summaryText = await fetchConversationSummary({ messages }) + if (summaryText) { + updateTopic({ ...currentTopic.current, name: summaryText }) + } + } + } }, { label: 'Rename', @@ -35,8 +46,12 @@ const TopicList: FC = ({ agent, activeTopic, setActiveTopic }) => { updateTopic({ ...currentTopic.current, name }) } } - }, - { + } + ] + + if (agent.topics.length > 1) { + items.push({ type: 'divider' }) + items.push({ label: 'Delete', danger: true, key: 'delete', @@ -46,8 +61,8 @@ const TopicList: FC = ({ agent, activeTopic, setActiveTopic }) => { currentTopic.current = null setActiveTopic(agent.topics[0]) } - } - ] + }) + } if (!showRightSidebar) { return null @@ -98,7 +113,7 @@ const TopicListItem = styled.div` const TopicTitle = styled.div` font-weight: bold; - margin-bottom: 5px; + margin-bottom: 10px; font-size: 14px; color: var(--color-text-1); ` diff --git a/src/renderer/src/services/agent.ts b/src/renderer/src/services/agent.ts index fcbd95cf..c0ebc0dc 100644 --- a/src/renderer/src/services/agent.ts +++ b/src/renderer/src/services/agent.ts @@ -1,3 +1,4 @@ +import { DEFAULT_TOPIC_NAME } from '@renderer/config/constant' import { Agent } from '@renderer/types' import { uuid } from '@renderer/utils' @@ -9,7 +10,7 @@ export function getDefaultAgent(): Agent { topics: [ { id: uuid(), - name: 'Default Topic', + name: DEFAULT_TOPIC_NAME, messages: [] } ] diff --git a/src/renderer/src/services/api.ts b/src/renderer/src/services/api.ts new file mode 100644 index 00000000..f56f1862 --- /dev/null +++ b/src/renderer/src/services/api.ts @@ -0,0 +1,67 @@ +import { Agent, Message, Topic } from '@renderer/types' +import { openaiProvider } from './provider' +import { uuid } from '@renderer/utils' +import { EVENT_NAMES, EventEmitter } from './event' +import { ChatCompletionMessageParam, ChatCompletionSystemMessageParam } from 'openai/resources' + +interface FetchChatCompletionParams { + message: Message + agent: Agent + topic: Topic + onResponse: (message: Message) => void +} + +export async function fetchChatCompletion({ message, agent, topic, onResponse }: FetchChatCompletionParams) { + const stream = await openaiProvider.chat.completions.create({ + model: 'Qwen/Qwen2-7B-Instruct', + messages: [{ role: 'user', content: message.content }], + stream: true + }) + + const _message: Message = { + id: uuid(), + role: 'agent', + content: '', + agentId: agent.id, + topicId: topic.id, + createdAt: 'now' + } + + let content = '' + + for await (const chunk of stream) { + content = content + (chunk.choices[0]?.delta?.content || '') + onResponse({ ..._message, content }) + } + + _message.content = content + + EventEmitter.emit(EVENT_NAMES.AI_CHAT_COMPLETION, _message) + + return _message +} + +interface FetchConversationSummaryParams { + messages: Message[] +} + +export async function fetchConversationSummary({ messages }: FetchConversationSummaryParams) { + const userMessages: ChatCompletionMessageParam[] = messages.map((message) => ({ + role: 'user', + content: message.content + })) + + const systemMessage: ChatCompletionSystemMessageParam = { + role: 'system', + content: + '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,回复内容不需要用引号引起来,不需要在结尾加上句号。' + } + + const response = await openaiProvider.chat.completions.create({ + model: 'Qwen/Qwen2-7B-Instruct', + messages: [systemMessage, ...userMessages], + stream: false + }) + + return response.choices[0].message?.content +} diff --git a/src/renderer/src/services/event.ts b/src/renderer/src/services/event.ts index c375208c..c20482cf 100644 --- a/src/renderer/src/services/event.ts +++ b/src/renderer/src/services/event.ts @@ -4,5 +4,6 @@ export const EventEmitter = new Emittery() export const EVENT_NAMES = { SEND_MESSAGE: 'SEND_MESSAGE', - AI_CHAT_COMPLETION: 'AI_CHAT_COMPLETION' + AI_CHAT_COMPLETION: 'AI_CHAT_COMPLETION', + AI_AUTO_RENAME: 'AI_AUTO_RENAME' } diff --git a/src/renderer/src/services/topic.ts b/src/renderer/src/services/topic.ts new file mode 100644 index 00000000..193394c3 --- /dev/null +++ b/src/renderer/src/services/topic.ts @@ -0,0 +1,7 @@ +import { Topic } from '@renderer/types' +import localforage from 'localforage' + +export async function getTopicMessages(id: string) { + const topic = await localforage.getItem(`topic:${id}`) + return topic ? topic.messages : [] +} diff --git a/src/renderer/src/store/agents.ts b/src/renderer/src/store/agents.ts index 1665884a..e12c8cb5 100644 --- a/src/renderer/src/store/agents.ts +++ b/src/renderer/src/store/agents.ts @@ -25,7 +25,6 @@ const agentsSlice = createSlice({ state.agents = state.agents.map((c) => (c.id === action.payload.id ? action.payload : c)) }, addTopic: (state, action: PayloadAction<{ agentId: string; topic: Topic }>) => { - console.debug(action.payload) state.agents = state.agents.map((agent) => agent.id === action.payload.agentId ? {