diff --git a/app/api/conversations/[conversationId]/name/route.ts b/app/api/conversations/[conversationId]/name/route.ts new file mode 100644 index 0000000..0b7cffe --- /dev/null +++ b/app/api/conversations/[conversationId]/name/route.ts @@ -0,0 +1,20 @@ +import { type NextRequest } from 'next/server' +import { NextResponse } from 'next/server' +import { client, getInfo } from '@/app/api/utils/common' + +export async function POST(request: NextRequest, { params }: { + params: { conversationId: string } +}) { + const body = await request.json() + const { + auto_generate, + name, + } = body + const { conversationId } = params + const { user } = getInfo(request) + + // auto generate name + const { data } = await client.renameConversation(conversationId, name, user, auto_generate) + console.log(conversationId, name, user, auto_generate) + return NextResponse.json(data) +} diff --git a/app/components/base/icons/public/data-set/index.tsx b/app/components/base/icons/public/data-set/index.tsx index cadbbdc..fa2acc3 100644 --- a/app/components/base/icons/public/data-set/index.tsx +++ b/app/components/base/icons/public/data-set/index.tsx @@ -2,7 +2,7 @@ // DON NOT EDIT IT MANUALLY import * as React from 'react' -import data from './DataSet.json' +import data from './data.json' import IconBase from '@/app/components/base/icons/IconBase' import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' diff --git a/app/components/base/icons/solid/general/check-circle/index.tsx b/app/components/base/icons/solid/general/check-circle/index.tsx index fe2cbfc..07a0ff1 100644 --- a/app/components/base/icons/solid/general/check-circle/index.tsx +++ b/app/components/base/icons/solid/general/check-circle/index.tsx @@ -2,7 +2,7 @@ // DON NOT EDIT IT MANUALLY import * as React from 'react' -import data from './CheckCircle.json' +import data from './data.json' import IconBase from '@/app/components/base/icons/IconBase' import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' diff --git a/app/components/index.tsx b/app/components/index.tsx index cc65547..9cc3f41 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -3,16 +3,16 @@ import type { FC } from 'react' import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import produce from 'immer' +import produce, { setAutoFreeze } from 'immer' import { useBoolean, useGetState } from 'ahooks' import useConversation from '@/hooks/use-conversation' import Toast from '@/app/components/base/toast' import Sidebar from '@/app/components/sidebar' import ConfigSence from '@/app/components/config-scence' import Header from '@/app/components/header' -import { fetchAppParams, fetchChatList, fetchConversations, sendChatMessage, updateFeedback } from '@/service' +import { fetchAppParams, fetchChatList, fetchConversations, generationConversationName, sendChatMessage, updateFeedback } from '@/service' import type { ConversationItem, Feedbacktype, IChatItem, PromptConfig, VisionFile, VisionSettings } from '@/types/app' -import { TransferMethod } from '@/types/app' +import { Resolution, TransferMethod } from '@/types/app' import Chat from '@/app/components/chat' import { setLocaleOnClient } from '@/i18n/client' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' @@ -20,6 +20,7 @@ import Loading from '@/app/components/base/loading' import { replaceVarWithValues, userInputsFormToPromptVariables } from '@/utils/prompt' import AppUnavailable from '@/app/components/app-unavailable' import { API_KEY, APP_ID, APP_INFO, isShowPrompt, promptTemplate } from '@/config' +import type { Annotation as AnnotationType } from '@/types/log' const Main: FC = () => { const { t } = useTranslation() @@ -36,13 +37,26 @@ const Main: FC = () => { const [inited, setInited] = useState(false) // in mobile, show sidebar by click button const [isShowSidebar, { setTrue: showSidebar, setFalse: hideSidebar }] = useBoolean(false) - const [visionConfig, setVisionConfig] = useState(undefined) + const [visionConfig, setVisionConfig] = useState({ + enabled: false, + number_limits: 2, + detail: Resolution.low, + transfer_methods: [TransferMethod.local_file], + }) useEffect(() => { if (APP_INFO?.title) document.title = `${APP_INFO.title} - Powered by Dify` }, [APP_INFO?.title]) + // onData change thought (the produce obj). https://github.com/immerjs/immer/issues/576 + useEffect(() => { + setAutoFreeze(false) + return () => { + setAutoFreeze(true) + } + }, []) + /* * conversation info */ @@ -50,6 +64,7 @@ const Main: FC = () => { conversationList, setConversationList, currConversationId, + getCurrConversationId, setCurrConversationId, getConversationIdFromStorage, isNewConversation, @@ -244,6 +259,7 @@ const Main: FC = () => { }, []) const [isResponsing, { setTrue: setResponsingTrue, setFalse: setResponsingFalse }] = useBoolean(false) + const [abortController, setAbortController] = useState(null) const { notify } = Toast const logError = (message: string) => { notify({ type: 'error', message }) @@ -267,6 +283,36 @@ const Main: FC = () => { return true } + const [controlFocus, setControlFocus] = useState(0) + const [openingSuggestedQuestions, setOpeningSuggestedQuestions] = useState([]) + const [messageTaskId, setMessageTaskId] = useState('') + const [hasStopResponded, setHasStopResponded, getHasStopResponded] = useGetState(false) + const [isResponsingConIsCurrCon, setIsResponsingConCurrCon, getIsResponsingConIsCurrCon] = useGetState(true) + const [userQuery, setUserQuery] = useState('') + + const updateCurrentQA = ({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }: { + responseItem: IChatItem + questionId: string + placeholderAnswerId: string + questionItem: IChatItem + }) => { + // closesure new list is outdated. + const newListWithAnswer = produce( + getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), + (draft) => { + if (!draft.find(item => item.id === questionId)) + draft.push({ ...questionItem }) + + draft.push({ ...responseItem }) + }) + setChatList(newListWithAnswer) + } + const handleSend = async (message: string, files?: VisionFile[]) => { if (isResponsing) { notify({ type: 'info', message: t('app.errorMessage.waitForResponse') }) @@ -309,23 +355,145 @@ const Main: FC = () => { const newList = [...getChatList(), questionItem, placeholderAnswerItem] setChatList(newList) + let isAgentMode = false + // answer - const responseItem = { + const responseItem: IChatItem = { id: `${Date.now()}`, content: '', + agent_thoughts: [], + message_files: [], isAnswer: true, } + let hasSetResponseId = false + const prevTempNewConversationId = getCurrConversationId() || '-1' let tempNewConversationId = '' + setResponsingTrue() sendChatMessage(data, { - onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId }: any) => { - responseItem.content = responseItem.content + message - responseItem.id = messageId + getAbortController: (abortController) => { + setAbortController(abortController) + }, + onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => { + if (!isAgentMode) { + responseItem.content = responseItem.content + message + } + else { + const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1] + if (lastThought) + lastThought.thought = lastThought.thought + message // need immer setAutoFreeze + } + if (messageId && !hasSetResponseId) { + responseItem.id = messageId + hasSetResponseId = true + } + if (isFirstMessage && newConversationId) tempNewConversationId = newConversationId - // closesure new list is outdated. + setMessageTaskId(taskId) + // has switched to other conversation + if (prevTempNewConversationId !== getCurrConversationId()) { + setIsResponsingConCurrCon(false) + return + } + updateCurrentQA({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }) + }, + async onCompleted(hasError?: boolean) { + if (hasError) + return + + if (getConversationIdChangeBecauseOfNew()) { + const { data: allConversations }: any = await fetchConversations() + const newItem: any = await generationConversationName(allConversations[0].id) + + const newAllConversations = produce(allConversations, (draft: any) => { + draft[0].name = newItem.name + }) + setConversationList(newAllConversations as any) + } + setConversationIdChangeBecauseOfNew(false) + resetNewConversationInputs() + setChatNotStarted() + setCurrConversationId(tempNewConversationId, APP_ID, true) + setResponsingFalse() + }, + onFile(file) { + const lastThought = responseItem.agent_thoughts?.[responseItem.agent_thoughts?.length - 1] + if (lastThought) + lastThought.message_files = [...(lastThought as any).message_files, { ...file }] + + updateCurrentQA({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }) + }, + onThought(thought) { + isAgentMode = true + const response = responseItem as any + if (thought.message_id && !hasSetResponseId) { + response.id = thought.message_id + hasSetResponseId = true + } + // responseItem.id = thought.message_id; + if (response.agent_thoughts.length === 0) { + response.agent_thoughts.push(thought) + } + else { + const lastThought = response.agent_thoughts[response.agent_thoughts.length - 1] + // thought changed but still the same thought, so update. + if (lastThought.id === thought.id) { + thought.thought = lastThought.thought + thought.message_files = lastThought.message_files + responseItem.agent_thoughts![response.agent_thoughts.length - 1] = thought + } + else { + responseItem.agent_thoughts!.push(thought) + } + } + // has switched to other conversation + if (prevTempNewConversationId !== getCurrConversationId()) { + setIsResponsingConCurrCon(false) + return false + } + + updateCurrentQA({ + responseItem, + questionId, + placeholderAnswerId, + questionItem, + }) + }, + onMessageEnd: (messageEnd) => { + if (messageEnd.metadata?.annotation_reply) { + responseItem.id = messageEnd.id + responseItem.annotation = ({ + id: messageEnd.metadata.annotation_reply.id, + authorName: messageEnd.metadata.annotation_reply.account.name, + } as AnnotationType) + const newListWithAnswer = produce( + getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), + (draft) => { + if (!draft.find(item => item.id === questionId)) + draft.push({ ...questionItem }) + + draft.push({ + ...responseItem, + }) + }) + setChatList(newListWithAnswer) + return + } + // not support show citation + // responseItem.citation = messageEnd.retriever_resources const newListWithAnswer = produce( getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId), (draft) => { @@ -336,19 +504,16 @@ const Main: FC = () => { }) setChatList(newListWithAnswer) }, - async onCompleted() { - setResponsingFalse() - if (!tempNewConversationId) - return + onMessageReplace: (messageReplace) => { + setChatList(produce( + getChatList(), + (draft) => { + const current = draft.find(item => item.id === messageReplace.id) - if (getConversationIdChangeBecauseOfNew()) { - const { data: conversations }: any = await fetchConversations() - setConversationList(conversations as ConversationItem[]) - } - setConversationIdChangeBecauseOfNew(false) - resetNewConversationInputs() - setChatNotStarted() - setCurrConversationId(tempNewConversationId, APP_ID, true) + if (current) + current.content = messageReplace.answer + }, + )) }, onError() { setResponsingFalse() diff --git a/hooks/use-conversation.ts b/hooks/use-conversation.ts index e24e6da..6a9c4ad 100644 --- a/hooks/use-conversation.ts +++ b/hooks/use-conversation.ts @@ -1,5 +1,6 @@ import { useState } from 'react' import produce from 'immer' +import { useGetState } from 'ahooks' import type { ConversationItem } from '@/types/app' const storageConversationIdKey = 'conversationIdInfo' @@ -7,7 +8,7 @@ const storageConversationIdKey = 'conversationIdInfo' type ConversationInfoType = Omit function useConversation() { const [conversationList, setConversationList] = useState([]) - const [currConversationId, doSetCurrConversationId] = useState('-1') + const [currConversationId, doSetCurrConversationId, getCurrConversationId] = useGetState('-1') // when set conversation id, we do not have set appId const setCurrConversationId = (id: string, appId: string, isSetToLocalStroge = true, newConversationName = '') => { doSetCurrConversationId(id) @@ -50,6 +51,7 @@ function useConversation() { conversationList, setConversationList, currConversationId, + getCurrConversationId, setCurrConversationId, getConversationIdFromStorage, isNewConversation, diff --git a/package.json b/package.json index 809eef3..a8666e1 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "axios": "^1.3.5", "classnames": "^2.3.2", "copy-to-clipboard": "^3.3.3", - "dify-client": "^2.1.0", + "dify-client": "^2.2.0", "eslint": "8.36.0", "eslint-config-next": "13.4.0", "eventsource-parser": "^1.0.0", diff --git a/service/index.ts b/service/index.ts index 126dc07..9053515 100644 --- a/service/index.ts +++ b/service/index.ts @@ -21,7 +21,7 @@ export const sendChatMessage = async (body: Record, { onData, onCom } export const fetchConversations = async () => { - return get('conversations', { params: { limit: 20, first_id: '' } }) + return get('conversations', { params: { limit: 100, first_id: '' } }) } export const fetchChatList = async (conversationId: string) => { @@ -36,3 +36,7 @@ export const fetchAppParams = async () => { export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }) => { return post(url, { body }) } + +export const generationConversationName = async (id: string) => { + return post(`conversations/${id}/name`, { body: { auto_generate: true } }) +} diff --git a/types/app.ts b/types/app.ts index 93d9691..c7593cb 100644 --- a/types/app.ts +++ b/types/app.ts @@ -1,4 +1,6 @@ +import type { Annotation } from './log' import type { Locale } from '@/i18n' +import type { ThoughtItem } from '@/app/components/chat/type' export type PromptVariable = { key: string @@ -74,9 +76,12 @@ export type IChatItem = { * More information about this message */ more?: MessageMore - isIntroduction?: boolean + annotation?: Annotation useCurrentUserAvatar?: boolean isOpeningStatement?: boolean + suggestedQuestions?: string[] + log?: { role: string; text: string }[] + agent_thoughts?: ThoughtItem[] message_files?: VisionFile[] } diff --git a/types/log.ts b/types/log.ts new file mode 100644 index 0000000..cae5da4 --- /dev/null +++ b/types/log.ts @@ -0,0 +1,16 @@ +export type LogAnnotation = { + content: string + account: { + id: string + name: string + email: string + } + created_at: number +} + +export type Annotation = { + id: string + authorName: string + logAnnotation?: LogAnnotation + created_at?: number +}