feat: ai auto rename topic
This commit is contained in:
parent
ceb816bc2a
commit
7f46e07368
@ -1,6 +1,6 @@
|
|||||||
.markdown {
|
.markdown {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
|
||||||
@ -89,6 +89,8 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
font-size: 90%;
|
||||||
|
display: inline-block;
|
||||||
font-family:
|
font-family:
|
||||||
ui-monospace,
|
ui-monospace,
|
||||||
SFMono-Regular,
|
SFMono-Regular,
|
||||||
@ -97,7 +99,5 @@
|
|||||||
Consolas,
|
Consolas,
|
||||||
Liberation Mono,
|
Liberation Mono,
|
||||||
monospace;
|
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 { Agent } from '@renderer/types'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import Inputbar from './Inputbar'
|
import Inputbar from './Inputbar'
|
||||||
import Conversations from './Conversations'
|
import Conversations from './Conversations'
|
||||||
import { Flex } from 'antd'
|
import { Flex } from 'antd'
|
||||||
import TopicList from './TopicList'
|
import TopicList from './TopicList'
|
||||||
import { useAgent } from '@renderer/hooks/useAgents'
|
import { useAgent } from '@renderer/hooks/useAgents'
|
||||||
|
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
agent: Agent
|
agent: Agent
|
||||||
@ -13,11 +14,7 @@ interface Props {
|
|||||||
|
|
||||||
const Chat: FC<Props> = (props) => {
|
const Chat: FC<Props> = (props) => {
|
||||||
const { agent } = useAgent(props.agent.id)
|
const { agent } = useAgent(props.agent.id)
|
||||||
const [activeTopic, setActiveTopic] = useState(agent.topics[0])
|
const { activeTopic, setActiveTopic } = useActiveTopic(agent)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setActiveTopic(agent.topics[0])
|
|
||||||
}, [agent])
|
|
||||||
|
|
||||||
if (!agent) {
|
if (!agent) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { openaiProvider } from '@renderer/services/provider'
|
|
||||||
import { Agent, Message, Topic } from '@renderer/types'
|
import { Agent, Message, Topic } from '@renderer/types'
|
||||||
import { runAsyncFunction, uuid } from '@renderer/utils'
|
|
||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
import { FC, useCallback, useEffect, useState } from 'react'
|
import { FC, useCallback, useEffect, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import MessageItem from './Message'
|
import MessageItem from './Message'
|
||||||
import { reverse } from 'lodash'
|
import { reverse } from 'lodash'
|
||||||
import hljs from 'highlight.js'
|
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 {
|
interface Props {
|
||||||
agent: Agent
|
agent: Agent
|
||||||
@ -17,6 +20,7 @@ interface Props {
|
|||||||
const Conversations: FC<Props> = ({ agent, topic }) => {
|
const Conversations: FC<Props> = ({ agent, topic }) => {
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
||||||
|
const { updateTopic } = useAgent(agent.id)
|
||||||
|
|
||||||
const onSendMessage = useCallback(
|
const onSendMessage = useCallback(
|
||||||
(message: Message) => {
|
(message: Message) => {
|
||||||
@ -30,59 +34,37 @@ const Conversations: FC<Props> = ({ agent, topic }) => {
|
|||||||
[messages, topic]
|
[messages, topic]
|
||||||
)
|
)
|
||||||
|
|
||||||
const fetchChatCompletion = useCallback(
|
const autoRenameTopic = useCallback(async () => {
|
||||||
async (message: Message) => {
|
if (topic.name === DEFAULT_TOPIC_NAME && messages.length >= 2) {
|
||||||
const stream = await openaiProvider.chat.completions.create({
|
const summaryText = await fetchConversationSummary({ messages })
|
||||||
model: 'Qwen/Qwen2-7B-Instruct',
|
if (summaryText) {
|
||||||
messages: [{ role: 'user', content: message.content }],
|
updateTopic({ ...topic, name: summaryText })
|
||||||
stream: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const _message: Message = {
|
|
||||||
id: uuid(),
|
|
||||||
role: 'agent',
|
|
||||||
content: '',
|
|
||||||
agentId: agent.id,
|
|
||||||
topicId: topic.id,
|
|
||||||
createdAt: 'now'
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let content = ''
|
}, [messages, topic, updateTopic])
|
||||||
|
|
||||||
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]
|
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
|
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
|
||||||
onSendMessage(msg)
|
onSendMessage(msg)
|
||||||
fetchChatCompletion(msg)
|
fetchChatCompletion({ agent, message: msg, topic, onResponse: setLastMessage })
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
|
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
|
||||||
setLastMessage(null)
|
setLastMessage(null)
|
||||||
onSendMessage(msg)
|
onSendMessage(msg)
|
||||||
})
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
||||||
|
}),
|
||||||
|
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic)
|
||||||
]
|
]
|
||||||
return () => unsubscribes.forEach((unsub) => unsub())
|
return () => unsubscribes.forEach((unsub) => unsub())
|
||||||
}, [fetchChatCompletion, onSendMessage])
|
}, [agent, autoRenameTopic, onSendMessage, topic])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
const _topic = await localforage.getItem<Topic>(`topic:${topic.id}`)
|
const messages = await getTopicMessages(topic.id)
|
||||||
setMessages(_topic ? _topic.messages : [])
|
setMessages(messages)
|
||||||
})
|
})
|
||||||
}, [topic])
|
}, [topic.id])
|
||||||
|
|
||||||
useEffect(() => hljs.highlightAll())
|
useEffect(() => hljs.highlightAll())
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
import { useAgent } from '@renderer/hooks/useAgents'
|
import { useAgent } from '@renderer/hooks/useAgents'
|
||||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
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 { Agent, Topic } from '@renderer/types'
|
||||||
import { Dropdown, MenuProps } from 'antd'
|
import { Dropdown, MenuProps } from 'antd'
|
||||||
import { FC, useRef } from 'react'
|
import { FC, useRef } from 'react'
|
||||||
@ -14,13 +16,22 @@ interface Props {
|
|||||||
|
|
||||||
const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
|
const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
|
||||||
const { showRightSidebar } = useShowRightSidebar()
|
const { showRightSidebar } = useShowRightSidebar()
|
||||||
const currentTopic = useRef<Topic | null>(null)
|
|
||||||
const { removeTopic, updateTopic } = useAgent(agent.id)
|
const { removeTopic, updateTopic } = useAgent(agent.id)
|
||||||
|
const currentTopic = useRef<Topic | null>(null)
|
||||||
|
|
||||||
const items: MenuProps['items'] = [
|
const items: MenuProps['items'] = [
|
||||||
{
|
{
|
||||||
label: 'AI Rename',
|
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',
|
label: 'Rename',
|
||||||
@ -35,8 +46,12 @@ const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
|
|||||||
updateTopic({ ...currentTopic.current, name })
|
updateTopic({ ...currentTopic.current, name })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{
|
]
|
||||||
|
|
||||||
|
if (agent.topics.length > 1) {
|
||||||
|
items.push({ type: 'divider' })
|
||||||
|
items.push({
|
||||||
label: 'Delete',
|
label: 'Delete',
|
||||||
danger: true,
|
danger: true,
|
||||||
key: 'delete',
|
key: 'delete',
|
||||||
@ -46,8 +61,8 @@ const TopicList: FC<Props> = ({ agent, activeTopic, setActiveTopic }) => {
|
|||||||
currentTopic.current = null
|
currentTopic.current = null
|
||||||
setActiveTopic(agent.topics[0])
|
setActiveTopic(agent.topics[0])
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
]
|
}
|
||||||
|
|
||||||
if (!showRightSidebar) {
|
if (!showRightSidebar) {
|
||||||
return null
|
return null
|
||||||
@ -98,7 +113,7 @@ const TopicListItem = styled.div`
|
|||||||
|
|
||||||
const TopicTitle = styled.div`
|
const TopicTitle = styled.div`
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
`
|
`
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { DEFAULT_TOPIC_NAME } from '@renderer/config/constant'
|
||||||
import { Agent } from '@renderer/types'
|
import { Agent } from '@renderer/types'
|
||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
|
|
||||||
@ -9,7 +10,7 @@ export function getDefaultAgent(): Agent {
|
|||||||
topics: [
|
topics: [
|
||||||
{
|
{
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
name: 'Default Topic',
|
name: DEFAULT_TOPIC_NAME,
|
||||||
messages: []
|
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 = {
|
export const EVENT_NAMES = {
|
||||||
SEND_MESSAGE: 'SEND_MESSAGE',
|
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))
|
state.agents = state.agents.map((c) => (c.id === action.payload.id ? action.payload : c))
|
||||||
},
|
},
|
||||||
addTopic: (state, action: PayloadAction<{ agentId: string; topic: Topic }>) => {
|
addTopic: (state, action: PayloadAction<{ agentId: string; topic: Topic }>) => {
|
||||||
console.debug(action.payload)
|
|
||||||
state.agents = state.agents.map((agent) =>
|
state.agents = state.agents.map((agent) =>
|
||||||
agent.id === action.payload.agentId
|
agent.id === action.payload.agentId
|
||||||
? {
|
? {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user