feat: ai auto rename topic
This commit is contained in:
parent
ceb816bc2a
commit
7f46e07368
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
1
src/renderer/src/config/constant.ts
Normal file
1
src/renderer/src/config/constant.ts
Normal file
@ -0,0 +1 @@
|
||||
export const DEFAULT_TOPIC_NAME = 'Default Topic'
|
||||
12
src/renderer/src/hooks/useTopic.ts
Normal file
12
src/renderer/src/hooks/useTopic.ts
Normal 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 }
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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())
|
||||
|
||||
|
||||
@ -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);
|
||||
`
|
||||
|
||||
@ -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: []
|
||||
}
|
||||
]
|
||||
|
||||
67
src/renderer/src/services/api.ts
Normal file
67
src/renderer/src/services/api.ts
Normal 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
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
|
||||
7
src/renderer/src/services/topic.ts
Normal file
7
src/renderer/src/services/topic.ts
Normal 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 : []
|
||||
}
|
||||
@ -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
|
||||
? {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user