feat: ai auto rename topic

This commit is contained in:
kangfenmao 2024-07-02 14:21:47 +08:00
parent ceb816bc2a
commit 7f46e07368
11 changed files with 140 additions and 58 deletions

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export const DEFAULT_TOPIC_NAME = 'Default Topic'

View File

@ -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 }
}

View File

@ -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> = (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

View File

@ -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<Props> = ({ agent, topic }) => {
const [messages, setMessages] = useState<Message[]>([])
const [lastMessage, setLastMessage] = useState<Message | null>(null)
const { updateTopic } = useAgent(agent.id)
const onSendMessage = useCallback(
(message: Message) => {
@ -30,59 +34,37 @@ const Conversations: FC<Props> = ({ 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:${topic.id}`)
setMessages(_topic ? _topic.messages : [])
const messages = await getTopicMessages(topic.id)
setMessages(messages)
})
}, [topic])
}, [topic.id])
useEffect(() => hljs.highlightAll())

View File

@ -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<Props> = ({ agent, activeTopic, setActiveTopic }) => {
const { showRightSidebar } = useShowRightSidebar()
const currentTopic = useRef<Topic | null>(null)
const { removeTopic, updateTopic } = useAgent(agent.id)
const currentTopic = useRef<Topic | null>(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<Props> = ({ 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<Props> = ({ 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);
`

View File

@ -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: []
}
]

View File

@ -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
}

View File

@ -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'
}

View File

@ -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>(`topic:${id}`)
return topic ? topic.messages : []
}

View File

@ -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
? {