feat: add message suggestions
This commit is contained in:
parent
c50ff4585a
commit
d8d4afbc0d
@ -70,6 +70,7 @@
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router": "6",
|
||||
"react-router-dom": "6",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"sass": "^1.77.2",
|
||||
|
||||
@ -151,3 +151,15 @@ body,
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background-color: #000;
|
||||
box-shadow:
|
||||
32px 0 #000,
|
||||
-32px 0 #000;
|
||||
position: relative;
|
||||
animation: flash 0.5s ease-out infinite alternate;
|
||||
}
|
||||
|
||||
@ -78,7 +78,8 @@ const resources = {
|
||||
'settings.conext_count.tip': 'The number of previous messages to keep in the context.',
|
||||
'settings.reset': 'Reset',
|
||||
'settings.set_as_default': 'Apply to default assistant',
|
||||
'settings.max': 'Max'
|
||||
'settings.max': 'Max',
|
||||
'suggestions.title': 'Suggested Questions'
|
||||
},
|
||||
apps: {
|
||||
title: 'Agents'
|
||||
@ -262,7 +263,8 @@ const resources = {
|
||||
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10,代码生成建议 5-10',
|
||||
'settings.reset': '重置',
|
||||
'settings.set_as_default': '应用到默认助手',
|
||||
'settings.max': '不限'
|
||||
'settings.max': '不限',
|
||||
'suggestions.title': '建议的问题'
|
||||
},
|
||||
apps: {
|
||||
title: '智能体'
|
||||
|
||||
@ -4,13 +4,14 @@ import localforage from 'localforage'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import MessageItem from './Message'
|
||||
import { reverse } from 'lodash'
|
||||
import { debounce, reverse } from 'lodash'
|
||||
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
|
||||
import { t } from 'i18next'
|
||||
import Suggestions from './Suggestions'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@ -93,9 +94,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
})
|
||||
}, [topic.id])
|
||||
|
||||
const scrollTop = useCallback(
|
||||
debounce(() => containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' }), 500, {
|
||||
leading: true,
|
||||
trailing: false
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
|
||||
}, [messages])
|
||||
setTimeout(scrollTop, 100)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [messages, lastMessage])
|
||||
|
||||
useEffect(() => {
|
||||
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
|
||||
@ -103,6 +113,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
|
||||
return (
|
||||
<Container id="messages" key={assistant.id} ref={containerRef}>
|
||||
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
|
||||
{lastMessage && <MessageItem message={lastMessage} />}
|
||||
{reverse([...messages]).map((message, index) => (
|
||||
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
|
||||
|
||||
119
src/renderer/src/pages/home/components/Suggestions.tsx
Normal file
119
src/renderer/src/pages/home/components/Suggestions.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Assistant, Message, Suggestion } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import { fetchSuggestions } from '@renderer/services/api'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
messages: Message[]
|
||||
lastMessage: Message | null
|
||||
}
|
||||
|
||||
const suggestionsMap = new Map<string, Suggestion[]>()
|
||||
|
||||
const Suggestions: FC<Props> = ({ assistant, messages, lastMessage }) => {
|
||||
const [suggestions, setSuggestions] = useState<Suggestion[]>(
|
||||
suggestionsMap.get(messages[messages.length - 1]?.id) || []
|
||||
)
|
||||
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
|
||||
|
||||
const onClick = (s: Suggestion) => {
|
||||
const message: Message = {
|
||||
id: uuid(),
|
||||
role: 'user',
|
||||
content: s.content,
|
||||
assistantId: assistant.id,
|
||||
topicId: assistant.topics[0].id || uuid(),
|
||||
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
status: 'success'
|
||||
}
|
||||
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
|
||||
setLoadingSuggestions(true)
|
||||
const _suggestions = await fetchSuggestions({ assistant, messages: [...messages, msg] })
|
||||
if (_suggestions.length) {
|
||||
setSuggestions(_suggestions)
|
||||
suggestionsMap.set(msg.id, _suggestions)
|
||||
}
|
||||
setLoadingSuggestions(false)
|
||||
})
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [assistant, messages])
|
||||
|
||||
useEffect(() => {
|
||||
setSuggestions(suggestionsMap.get(messages[messages.length - 1]?.id) || [])
|
||||
}, [messages])
|
||||
|
||||
if (lastMessage) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (loadingSuggestions) {
|
||||
return (
|
||||
<Container>
|
||||
<BeatLoader color="var(--color-text-2)" size="10" />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SuggestionsContainer>
|
||||
{suggestions.map((s, i) => (
|
||||
<SuggestionItem key={i} onClick={() => onClick(s)}>
|
||||
{s.content} →
|
||||
</SuggestionItem>
|
||||
))}
|
||||
</SuggestionsContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
padding-left: 55px;
|
||||
`
|
||||
|
||||
const SuggestionsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
`
|
||||
|
||||
const SuggestionItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
padding: 7px 15px;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background-mute);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
`
|
||||
|
||||
export default Suggestions
|
||||
@ -71,7 +71,8 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
||||
provider: _provider.id,
|
||||
group: getDefaultGroupName(model.id),
|
||||
// @ts-ignore name
|
||||
description: model?.description
|
||||
description: model?.description,
|
||||
owned_by: model?.owned_by
|
||||
}))
|
||||
)
|
||||
setLoading(false)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Assistant, Message, Provider } from '@renderer/types'
|
||||
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
||||
import OpenAI from 'openai'
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
import { getDefaultModel, getTopNamingModel } from './assistant'
|
||||
@ -134,6 +134,28 @@ export default class ProviderSDK {
|
||||
}
|
||||
}
|
||||
|
||||
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
|
||||
const model = assistant.model
|
||||
|
||||
if (!model) {
|
||||
return []
|
||||
}
|
||||
|
||||
const response: any = await this.openaiSdk.request({
|
||||
method: 'post',
|
||||
path: '/advice_questions',
|
||||
body: {
|
||||
messages: messages.filter((m) => m.role === 'user').map((m) => ({ role: m.role, content: m.content })),
|
||||
model: model.id,
|
||||
max_tokens: 0,
|
||||
temperature: 0,
|
||||
n: 0
|
||||
}
|
||||
})
|
||||
|
||||
return response?.questions?.map((q: any) => ({ content: q })) || []
|
||||
}
|
||||
|
||||
public async check(): Promise<{ valid: boolean; error: Error | null }> {
|
||||
const model = this.provider.models[0]
|
||||
const body = {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Message, Provider, Topic } from '@renderer/types'
|
||||
import { Assistant, Message, Provider, Suggestion, Topic } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
@ -109,6 +109,34 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages:
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSuggestions({
|
||||
messages,
|
||||
assistant
|
||||
}: {
|
||||
messages: Message[]
|
||||
assistant: Assistant
|
||||
}): Promise<Suggestion[]> {
|
||||
console.debug('fetchSuggestions', messages, assistant)
|
||||
const provider = getAssistantProvider(assistant)
|
||||
const providerSdk = new ProviderSDK(provider)
|
||||
console.debug('fetchSuggestions', provider)
|
||||
const model = assistant.model
|
||||
|
||||
if (!model) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (model.owned_by !== 'graphrag') {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
return await providerSdk.suggestions(messages, assistant)
|
||||
} catch (error: any) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkApi(provider: Provider) {
|
||||
const model = provider.models[0]
|
||||
const key = 'api-check'
|
||||
|
||||
@ -36,7 +36,7 @@ export function getTranslateModel() {
|
||||
return store.getState().llm.translateModel
|
||||
}
|
||||
|
||||
export function getAssistantProvider(assistant: Assistant) {
|
||||
export function getAssistantProvider(assistant: Assistant): Provider {
|
||||
const providers = store.getState().llm.providers
|
||||
const provider = providers.find((p) => p.id === assistant.model?.provider)
|
||||
return provider || getDefaultProvider()
|
||||
|
||||
@ -55,6 +55,7 @@ export type Model = {
|
||||
provider: string
|
||||
name: string
|
||||
group: string
|
||||
owned_by?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
@ -66,3 +67,7 @@ export type SystemAssistant = {
|
||||
prompt: string
|
||||
group: string
|
||||
}
|
||||
|
||||
export type Suggestion = {
|
||||
content: string
|
||||
}
|
||||
|
||||
11
yarn.lock
11
yarn.lock
@ -3479,6 +3479,7 @@ __metadata:
|
||||
react-redux: "npm:^9.1.2"
|
||||
react-router: "npm:6"
|
||||
react-router-dom: "npm:6"
|
||||
react-spinners: "npm:^0.14.1"
|
||||
react-syntax-highlighter: "npm:^15.5.0"
|
||||
redux-persist: "npm:^6.0.0"
|
||||
sass: "npm:^1.77.2"
|
||||
@ -8601,6 +8602,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-spinners@npm:^0.14.1":
|
||||
version: 0.14.1
|
||||
resolution: "react-spinners@npm:0.14.1"
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: 10c0/5b3c101f789716331a0b6afad156293fb9aa05620e65494753001afcdb611788057f379b5979b34d570d527fa978003293266b59db505bf2d243ebab899ceeda
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-syntax-highlighter@npm:^15.5.0":
|
||||
version: 15.5.0
|
||||
resolution: "react-syntax-highlighter@npm:15.5.0"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user