feat: add message suggestions
This commit is contained in:
parent
c50ff4585a
commit
d8d4afbc0d
@ -70,6 +70,7 @@
|
|||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-router": "6",
|
"react-router": "6",
|
||||||
"react-router-dom": "6",
|
"react-router-dom": "6",
|
||||||
|
"react-spinners": "^0.14.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"sass": "^1.77.2",
|
"sass": "^1.77.2",
|
||||||
|
|||||||
@ -151,3 +151,15 @@ body,
|
|||||||
padding-bottom: 12px;
|
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.conext_count.tip': 'The number of previous messages to keep in the context.',
|
||||||
'settings.reset': 'Reset',
|
'settings.reset': 'Reset',
|
||||||
'settings.set_as_default': 'Apply to default assistant',
|
'settings.set_as_default': 'Apply to default assistant',
|
||||||
'settings.max': 'Max'
|
'settings.max': 'Max',
|
||||||
|
'suggestions.title': 'Suggested Questions'
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: 'Agents'
|
title: 'Agents'
|
||||||
@ -262,7 +263,8 @@ const resources = {
|
|||||||
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10,代码生成建议 5-10',
|
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10,代码生成建议 5-10',
|
||||||
'settings.reset': '重置',
|
'settings.reset': '重置',
|
||||||
'settings.set_as_default': '应用到默认助手',
|
'settings.set_as_default': '应用到默认助手',
|
||||||
'settings.max': '不限'
|
'settings.max': '不限',
|
||||||
|
'suggestions.title': '建议的问题'
|
||||||
},
|
},
|
||||||
apps: {
|
apps: {
|
||||||
title: '智能体'
|
title: '智能体'
|
||||||
|
|||||||
@ -4,13 +4,14 @@ import localforage from 'localforage'
|
|||||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
import { FC, useCallback, useEffect, useRef, 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 { debounce, reverse } from 'lodash'
|
||||||
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
|
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
|
||||||
import LocalStorage from '@renderer/services/storage'
|
import LocalStorage from '@renderer/services/storage'
|
||||||
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
|
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
|
import Suggestions from './Suggestions'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
@ -93,9 +94,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
|||||||
})
|
})
|
||||||
}, [topic.id])
|
}, [topic.id])
|
||||||
|
|
||||||
|
const scrollTop = useCallback(
|
||||||
|
debounce(() => containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' }), 500, {
|
||||||
|
leading: true,
|
||||||
|
trailing: false
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
|
setTimeout(scrollTop, 100)
|
||||||
}, [messages])
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [messages, lastMessage])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
|
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
|
||||||
@ -103,6 +113,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container id="messages" key={assistant.id} ref={containerRef}>
|
<Container id="messages" key={assistant.id} ref={containerRef}>
|
||||||
|
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
|
||||||
{lastMessage && <MessageItem message={lastMessage} />}
|
{lastMessage && <MessageItem message={lastMessage} />}
|
||||||
{reverse([...messages]).map((message, index) => (
|
{reverse([...messages]).map((message, index) => (
|
||||||
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
|
<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,
|
provider: _provider.id,
|
||||||
group: getDefaultGroupName(model.id),
|
group: getDefaultGroupName(model.id),
|
||||||
// @ts-ignore name
|
// @ts-ignore name
|
||||||
description: model?.description
|
description: model?.description,
|
||||||
|
owned_by: model?.owned_by
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
setLoading(false)
|
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 OpenAI from 'openai'
|
||||||
import Anthropic from '@anthropic-ai/sdk'
|
import Anthropic from '@anthropic-ai/sdk'
|
||||||
import { getDefaultModel, getTopNamingModel } from './assistant'
|
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 }> {
|
public async check(): Promise<{ valid: boolean; error: Error | null }> {
|
||||||
const model = this.provider.models[0]
|
const model = this.provider.models[0]
|
||||||
const body = {
|
const body = {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { setGenerating } from '@renderer/store/runtime'
|
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 { uuid } from '@renderer/utils'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import {
|
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) {
|
export async function checkApi(provider: Provider) {
|
||||||
const model = provider.models[0]
|
const model = provider.models[0]
|
||||||
const key = 'api-check'
|
const key = 'api-check'
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export function getTranslateModel() {
|
|||||||
return store.getState().llm.translateModel
|
return store.getState().llm.translateModel
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAssistantProvider(assistant: Assistant) {
|
export function getAssistantProvider(assistant: Assistant): Provider {
|
||||||
const providers = store.getState().llm.providers
|
const providers = store.getState().llm.providers
|
||||||
const provider = providers.find((p) => p.id === assistant.model?.provider)
|
const provider = providers.find((p) => p.id === assistant.model?.provider)
|
||||||
return provider || getDefaultProvider()
|
return provider || getDefaultProvider()
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export type Model = {
|
|||||||
provider: string
|
provider: string
|
||||||
name: string
|
name: string
|
||||||
group: string
|
group: string
|
||||||
|
owned_by?: string
|
||||||
description?: string
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,3 +67,7 @@ export type SystemAssistant = {
|
|||||||
prompt: string
|
prompt: string
|
||||||
group: 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-redux: "npm:^9.1.2"
|
||||||
react-router: "npm:6"
|
react-router: "npm:6"
|
||||||
react-router-dom: "npm:6"
|
react-router-dom: "npm:6"
|
||||||
|
react-spinners: "npm:^0.14.1"
|
||||||
react-syntax-highlighter: "npm:^15.5.0"
|
react-syntax-highlighter: "npm:^15.5.0"
|
||||||
redux-persist: "npm:^6.0.0"
|
redux-persist: "npm:^6.0.0"
|
||||||
sass: "npm:^1.77.2"
|
sass: "npm:^1.77.2"
|
||||||
@ -8601,6 +8602,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-syntax-highlighter@npm:^15.5.0":
|
||||||
version: 15.5.0
|
version: 15.5.0
|
||||||
resolution: "react-syntax-highlighter@npm:15.5.0"
|
resolution: "react-syntax-highlighter@npm:15.5.0"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user