Commit 7c54cd6b by Xianquan Committed by GitHub

fix(chat): align share chat history controls (#7195)

* fix(chat): align share chat history controls

* fix(chat): prevent action label selection

* chore: update pro submodule

* pro submodule
parent 4d90b720
...@@ -45,11 +45,12 @@ export type GetHistoriesResponseType = z.infer<typeof GetHistoriesResponseSchema ...@@ -45,11 +45,12 @@ export type GetHistoriesResponseType = z.infer<typeof GetHistoriesResponseSchema
const GetHistoryStatusPropsSchema = { const GetHistoryStatusPropsSchema = {
chatIds: z.array(z.string().min(1)).min(1).max(200).describe('需要刷新状态的会话 ID 列表') chatIds: z.array(z.string().min(1)).min(1).max(200).describe('需要刷新状态的会话 ID 列表')
}; };
export const GetHistoryStatusBodyRawSchema = createOutLinkChatTargetInputSchema( export const GetHistoryStatusBodyRawSchema = createOptionalOutLinkChatTargetInputSchema(
GetHistoryStatusPropsSchema GetHistoryStatusPropsSchema
); );
export const GetHistoryStatusBodySchema = export const GetHistoryStatusBodySchema = GetHistoryStatusBodyRawSchema.transform(
GetHistoryStatusBodyRawSchema.transform(transformChatTargetInput); transformOptionalChatTargetInput
);
export type GetHistoryStatusBodyType = z.infer<typeof GetHistoryStatusBodyRawSchema>; export type GetHistoryStatusBodyType = z.infer<typeof GetHistoryStatusBodyRawSchema>;
export type GetHistoryStatusBodyRuntimeType = z.infer<typeof GetHistoryStatusBodySchema>; export type GetHistoryStatusBodyRuntimeType = z.infer<typeof GetHistoryStatusBodySchema>;
...@@ -82,9 +83,10 @@ const UpdateHistoryPropsSchema = { ...@@ -82,9 +83,10 @@ const UpdateHistoryPropsSchema = {
top: z.boolean().optional().describe('是否置顶') top: z.boolean().optional().describe('是否置顶')
}; };
export const UpdateHistoryBodyRawSchema = export const UpdateHistoryBodyRawSchema =
createOutLinkChatTargetInputSchema(UpdateHistoryPropsSchema); createOptionalOutLinkChatTargetInputSchema(UpdateHistoryPropsSchema);
export const UpdateHistoryBodySchema = export const UpdateHistoryBodySchema = UpdateHistoryBodyRawSchema.transform(
UpdateHistoryBodyRawSchema.transform(transformChatTargetInput); transformOptionalChatTargetInput
);
export type UpdateHistoryBodyType = z.infer<typeof UpdateHistoryBodyRawSchema>; export type UpdateHistoryBodyType = z.infer<typeof UpdateHistoryBodyRawSchema>;
export type UpdateHistoryBodyRuntimeType = z.infer<typeof UpdateHistoryBodySchema>; export type UpdateHistoryBodyRuntimeType = z.infer<typeof UpdateHistoryBodySchema>;
......
import React, { useState, useMemo, useCallback } from 'react'; import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react';
import { type BoxProps } from '@chakra-ui/react'; import { type BoxProps } from '@chakra-ui/react';
import { useAudioPlay } from '@/web/common/utils/voice'; import { useAudioPlay } from '@/web/common/utils/voice';
import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
...@@ -27,7 +27,7 @@ import { useCreation } from 'ahooks'; ...@@ -27,7 +27,7 @@ import { useCreation } from 'ahooks';
import type { ChatTypeEnum } from './constants'; import type { ChatTypeEnum } from './constants';
import type { ChatQuickAppType } from '@fastgpt/global/core/chat/setting/type'; import type { ChatQuickAppType } from '@fastgpt/global/core/chat/setting/type';
import { WorkflowRuntimeContextProvider } from '@/components/core/chat/ChatContainer/context/workflowRuntimeContext'; import { WorkflowRuntimeContextProvider } from '@/components/core/chat/ChatContainer/context/workflowRuntimeContext';
import { type ChatSourceTarget, toChatApiTarget } from '@/web/core/chat/utils'; import { getChatSourceKey, type ChatSourceTarget, toChatApiTarget } from '@/web/core/chat/utils';
import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants'; import { ChatSourceTypeEnum } from '@fastgpt/global/core/chat/constants';
export type ChatProviderProps = { export type ChatProviderProps = {
...@@ -195,6 +195,10 @@ const Provider = ({ ...@@ -195,6 +195,10 @@ const Provider = ({
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords); const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords);
const resolvedChatTarget = useMemo(() => toChatApiTarget(sourceTarget), [sourceTarget]); const resolvedChatTarget = useMemo(() => toChatApiTarget(sourceTarget), [sourceTarget]);
const audioScopeKey = useMemo(
() => `${getChatSourceKey(sourceTarget)}:${chatId}`,
[sourceTarget, chatId]
);
const resolvedAppId = useMemo( const resolvedAppId = useMemo(
() => (sourceTarget.sourceType === ChatSourceTypeEnum.app ? sourceTarget.sourceId : undefined), () => (sourceTarget.sourceType === ChatSourceTypeEnum.app ? sourceTarget.sourceId : undefined),
[sourceTarget] [sourceTarget]
...@@ -217,6 +221,15 @@ const Provider = ({ ...@@ -217,6 +221,15 @@ const Provider = ({
...formatOutLinkAuth ...formatOutLinkAuth
}); });
const lastAudioScopeKeyRef = useRef(audioScopeKey);
useEffect(() => {
if (lastAudioScopeKeyRef.current === audioScopeKey) return;
lastAudioScopeKeyRef.current = audioScopeKey;
cancelAudio();
setAudioPlayingChatId(undefined);
}, [audioScopeKey, cancelAudio]);
const autoTTSResponse = const autoTTSResponse =
enableTTS && enableTTS &&
whisperConfig?.open && whisperConfig?.open &&
......
...@@ -94,6 +94,7 @@ const AIChatBubbleActions = ({ ...@@ -94,6 +94,7 @@ const AIChatBubbleActions = ({
p={'4px'} p={'4px'}
cursor={'pointer'} cursor={'pointer'}
color={'myGray.400'} color={'myGray.400'}
userSelect={'none'}
transition={footerActionTransition} transition={footerActionTransition}
_hover={footerActionHoverStyle} _hover={footerActionHoverStyle}
onClick={onOpenWholeModal} onClick={onOpenWholeModal}
...@@ -148,6 +149,7 @@ const AIChatBubbleActions = ({ ...@@ -148,6 +149,7 @@ const AIChatBubbleActions = ({
p={'4px'} p={'4px'}
cursor={'pointer'} cursor={'pointer'}
color={'myGray.400'} color={'myGray.400'}
userSelect={'none'}
_hover={{ color: 'primary.600' }} _hover={{ color: 'primary.600' }}
onClick={onOpenSandboxModal} onClick={onOpenSandboxModal}
> >
......
...@@ -49,6 +49,9 @@ const AppChatWindow = () => { ...@@ -49,6 +49,9 @@ const AppChatWindow = () => {
const forbidLoadChatRef = useContextSelector(ChatContext, (v) => v.forbidLoadChat); const forbidLoadChatRef = useContextSelector(ChatContext, (v) => v.forbidLoadChat);
const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider);
const currentHistory = useContextSelector(ChatContext, (v) =>
v.histories.find((item) => item.chatId === chatId && item.appId === appId)
);
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin); const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite); const isShowCite = useContextSelector(ChatItemContext, (v) => v.isShowCite);
...@@ -57,6 +60,7 @@ const AppChatWindow = () => { ...@@ -57,6 +60,7 @@ const AppChatWindow = () => {
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const isCurrentChatReady = chatBoxData.appId === appId && chatBoxData.chatId === chatId; const isCurrentChatReady = chatBoxData.appId === appId && chatBoxData.chatId === chatId;
const chatWindowTitle = getDisplayHistoryTitle({ const chatWindowTitle = getDisplayHistoryTitle({
customTitle: currentHistory?.customTitle,
title: isCurrentChatReady ? chatBoxData.title : undefined, title: isCurrentChatReady ? chatBoxData.title : undefined,
fallbackTitle: t('common:core.chat.New Chat') fallbackTitle: t('common:core.chat.New Chat')
}); });
......
...@@ -87,6 +87,9 @@ const HomeChatWindow = () => { ...@@ -87,6 +87,9 @@ const HomeChatWindow = () => {
const forbidLoadChatRef = useContextSelector(ChatContext, (v) => v.forbidLoadChat); const forbidLoadChatRef = useContextSelector(ChatContext, (v) => v.forbidLoadChat);
const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider); const onOpenSlider = useContextSelector(ChatContext, (v) => v.onOpenSlider);
const currentHistory = useContextSelector(ChatContext, (v) =>
v.histories.find((item) => item.chatId === chatId && item.appId === appId)
);
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData); const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData); const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
...@@ -105,6 +108,7 @@ const HomeChatWindow = () => { ...@@ -105,6 +108,7 @@ const HomeChatWindow = () => {
const isCurrentChatReady = chatBoxData.appId === appId && chatBoxData.chatId === chatId; const isCurrentChatReady = chatBoxData.appId === appId && chatBoxData.chatId === chatId;
const chatWindowTitle = getDisplayHistoryTitle({ const chatWindowTitle = getDisplayHistoryTitle({
customTitle: currentHistory?.customTitle,
title: isCurrentChatReady ? chatBoxData.title : undefined, title: isCurrentChatReady ? chatBoxData.title : undefined,
fallbackTitle: t('common:core.chat.New Chat') fallbackTitle: t('common:core.chat.New Chat')
}); });
......
...@@ -160,12 +160,14 @@ export const useSandboxStatus = ({ ...@@ -160,12 +160,14 @@ export const useSandboxStatus = ({
appId, appId,
chatTarget, chatTarget,
chatId, chatId,
outLinkAuthData outLinkAuthData,
enabled = true
}: { }: {
appId?: string; appId?: string;
chatTarget?: ChatTargetInputType; chatTarget?: ChatTargetInputType;
chatId: string; chatId: string;
outLinkAuthData?: OutLinkChatAuthProps; outLinkAuthData?: OutLinkChatAuthProps;
enabled?: boolean;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [apiSandboxStatus, setApiSandboxStatus] = useState({ const [apiSandboxStatus, setApiSandboxStatus] = useState({
...@@ -174,10 +176,10 @@ export const useSandboxStatus = ({ ...@@ -174,10 +176,10 @@ export const useSandboxStatus = ({
exists: false exists: false
}); });
const sandboxTarget = useMemo( const sandboxTarget = useMemo(
() => resolveSandboxTarget({ appId, chatTarget }), () => (enabled ? resolveSandboxTarget({ appId, chatTarget }) : undefined),
[appId, chatTarget?.appId, chatTarget?.skillId] [appId, chatTarget?.appId, chatTarget?.skillId, enabled]
); );
const sandboxTargetId = getSandboxTargetId(sandboxTarget); const sandboxTargetId = sandboxTarget ? getSandboxTargetId(sandboxTarget) : '';
const chatRecords = useContextSelector(ChatRecordContext, (v) => { const chatRecords = useContextSelector(ChatRecordContext, (v) => {
return v.chatRecords; return v.chatRecords;
...@@ -193,7 +195,7 @@ export const useSandboxStatus = ({ ...@@ -193,7 +195,7 @@ export const useSandboxStatus = ({
}, [chatRecords, isChatRecordsLoaded]); }, [chatRecords, isChatRecordsLoaded]);
useEffect(() => { useEffect(() => {
if (!sandboxTargetId || !chatId) return; if (!sandboxTarget || !sandboxTargetId || !chatId) return;
let cancelled = false; let cancelled = false;
checkSandboxExist({ ...sandboxTarget, chatId, outLinkAuthData }) checkSandboxExist({ ...sandboxTarget, chatId, outLinkAuthData })
.then((result) => { .then((result) => {
......
...@@ -60,7 +60,8 @@ const ToolMenu = ({ ...@@ -60,7 +60,8 @@ const ToolMenu = ({
const { sandboxExists, setSandboxExists, SandboxEntryIcon } = useSandboxStatus({ const { sandboxExists, setSandboxExists, SandboxEntryIcon } = useSandboxStatus({
appId: isShareAuthReady ? currentAppId : '', appId: isShareAuthReady ? currentAppId : '',
chatId, chatId,
outLinkAuthData outLinkAuthData,
enabled: isShareAuthReady
}); });
// UI Hook: 负责弹窗渲染 // UI Hook: 负责弹窗渲染
......
...@@ -12,6 +12,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem'; ...@@ -12,6 +12,7 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time'; import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatGenerateStatusEnum } from '@fastgpt/global/core/chat/constants'; import { ChatGenerateStatusEnum } from '@fastgpt/global/core/chat/constants';
import { getDisplayHistoryTitle } from '@/web/core/chat/context/historyTitleUtils';
const ChatSliderList = () => { const ChatSliderList = () => {
const { isPc } = useSystem(); const { isPc } = useSystem();
...@@ -31,7 +32,6 @@ const ChatSliderList = () => { ...@@ -31,7 +32,6 @@ const ChatSliderList = () => {
const concatHistory = useMemo(() => { const concatHistory = useMemo(() => {
const newChatTitle = t('common:core.chat.New Chat'); const newChatTitle = t('common:core.chat.New Chat');
const getHistoryDisplayTitle = (title?: string) => title?.trim() || newChatTitle;
const scopedHistories = histories.filter((item) => item.appId === appId); const scopedHistories = histories.filter((item) => item.appId === appId);
const formatHistories: { const formatHistories: {
...@@ -50,11 +50,14 @@ const ChatSliderList = () => { ...@@ -50,11 +50,14 @@ const ChatSliderList = () => {
chatBoxData.appId === item.appId; chatBoxData.appId === item.appId;
const customTitle = item.customTitle?.trim() ? item.customTitle : undefined; const customTitle = item.customTitle?.trim() ? item.customTitle : undefined;
const realtimeTitle = chatBoxData.title?.trim() ? chatBoxData.title : undefined; const realtimeTitle = chatBoxData.title?.trim() ? chatBoxData.title : undefined;
const title = (isActiveChat ? realtimeTitle : undefined) || customTitle || item.title;
return { return {
id: item.chatId, id: item.chatId,
title: getHistoryDisplayTitle(title), title: getDisplayHistoryTitle({
customTitle,
title: (isActiveChat ? realtimeTitle : undefined) || item.title,
fallbackTitle: newChatTitle
}),
customTitle, customTitle,
top: item.top, top: item.top,
updateTime: item.updateTime, updateTime: item.updateTime,
...@@ -76,7 +79,10 @@ const ChatSliderList = () => { ...@@ -76,7 +79,10 @@ const ChatSliderList = () => {
isTemporary?: boolean; isTemporary?: boolean;
} = { } = {
id: activeChatId, id: activeChatId,
title: getHistoryDisplayTitle(chatBoxData.chatId === activeChatId ? chatBoxData.title : ''), title: getDisplayHistoryTitle({
title: chatBoxData.chatId === activeChatId ? chatBoxData.title : '',
fallbackTitle: newChatTitle
}),
updateTime: new Date(), updateTime: new Date(),
isTemporary: true, isTemporary: true,
chatGenerateStatus: chatGenerateStatus:
......
import type { NextApiResponse } from 'next'; import type { NextApiResponse } from 'next';
import { UpdateHistoryBodySchema } from '@fastgpt/global/openapi/core/chat/history/api'; import { UpdateHistoryBodySchema } from '@fastgpt/global/openapi/core/chat/history/api';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { authChatTargetCrud } from '@/service/support/permission/auth/chat';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { type ApiRequestProps } from '@fastgpt/service/type/next'; import { type ApiRequestProps } from '@fastgpt/service/type/next';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError'; import { parseApiInput } from '@fastgpt/service/common/zod/requestParseError';
import { buildChatSourceQuery } from '@fastgpt/service/core/chat/source'; import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { buildChatHistoryMatch } from '@/service/core/chat/history';
/* update chat history: title, customTitle, top */ /* update chat history: title, customTitle, top */
export async function handler(req: ApiRequestProps, res: NextApiResponse) { export async function handler(req: ApiRequestProps, res: NextApiResponse) {
const { sourceType, sourceId, chatId, title, customTitle, top } = parseApiInput({ const {
sourceType,
sourceId,
chatId,
title,
customTitle,
top,
shareId,
outLinkUid,
teamId,
teamToken
} = parseApiInput({
req, req,
bodySchema: UpdateHistoryBodySchema bodySchema: UpdateHistoryBodySchema
}).body; }).body;
await authChatTargetCrud({
const match = await buildChatHistoryMatch({
req, req,
authToken: true,
authApiKey: true,
sourceType, sourceType,
sourceId, sourceId,
chatId, chatId,
shareId,
outLinkUid,
teamId,
teamToken,
per: WritePermissionVal per: WritePermissionVal
}); });
if (!match) return Promise.reject(ChatErrEnum.unAuthChat);
await MongoChat.updateOne( await MongoChat.updateOne(
{ ...buildChatSourceQuery({ sourceType, sourceId }), chatId }, { ...match, chatId },
{ {
updateTime: new Date(), updateTime: new Date(),
...(title !== undefined && { title }), ...(title !== undefined && { title }),
......
...@@ -49,6 +49,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar'; ...@@ -49,6 +49,7 @@ import Avatar from '@fastgpt/web/components/common/Avatar';
import { getAppChatSourceKey } from '@/web/core/chat/utils'; import { getAppChatSourceKey } from '@/web/core/chat/utils';
import { useAppChatGenerateStatusSync } from '@/pageComponents/chat/ChatWindow/useAppChatGenerateStatusSync'; import { useAppChatGenerateStatusSync } from '@/pageComponents/chat/ChatWindow/useAppChatGenerateStatusSync';
import { postMarkChatRead } from '@/web/core/chat/history/api'; import { postMarkChatRead } from '@/web/core/chat/history/api';
import { useSandboxEditor, useSandboxStatus } from '@/pageComponents/chat/SandboxEditor/hook';
const logger = getLogger(LogCategories.MODULE.CHAT.ITEM); const logger = getLogger(LogCategories.MODULE.CHAT.ITEM);
...@@ -115,7 +116,11 @@ const OutLink = (props: Props) => { ...@@ -115,7 +116,11 @@ const OutLink = (props: Props) => {
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded); const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded);
const onChatGenerateStatusChange = useAppChatGenerateStatusSync(); const onChatGenerateStatusChange = useAppChatGenerateStatusSync();
const currentHistory = useContextSelector(ChatContext, (v) =>
v.histories.find((item) => item.chatId === chatId && item.appId === appId)
);
const chatWindowTitle = getDisplayHistoryTitle({ const chatWindowTitle = getDisplayHistoryTitle({
customTitle: currentHistory?.customTitle,
title: chatBoxData.title, title: chatBoxData.title,
fallbackTitle: t('common:core.chat.New Chat') fallbackTitle: t('common:core.chat.New Chat')
}); });
...@@ -159,6 +164,19 @@ const OutLink = (props: Props) => { ...@@ -159,6 +164,19 @@ const OutLink = (props: Props) => {
); );
const mobileHeaderAppName = props.appName || data?.app?.name || chatBoxData.app.name; const mobileHeaderAppName = props.appName || data?.app?.name || chatBoxData.app.name;
const mobileHeaderAppAvatar = props.appAvatar || data?.app?.avatar || chatBoxData.app.avatar; const mobileHeaderAppAvatar = props.appAvatar || data?.app?.avatar || chatBoxData.app.avatar;
const isShareAuthReady = !!outLinkAuthData.shareId && !!outLinkAuthData.outLinkUid;
const { SandboxEntryIcon } = useSandboxStatus({
appId: isShareAuthReady ? appId : '',
chatId,
outLinkAuthData,
enabled: isShareAuthReady
});
const { SandboxEditorModal, onOpenSandboxModal } = useSandboxEditor({
appId,
chatId,
outLinkAuthData,
enabled: isShareAuthReady
});
useEffect(() => { useEffect(() => {
if (initSign.current === false && data && isChatRecordsLoaded) { if (initSign.current === false && data && isChatRecordsLoaded) {
...@@ -312,13 +330,12 @@ const OutLink = (props: Props) => { ...@@ -312,13 +330,12 @@ const OutLink = (props: Props) => {
{/* header */} {/* header */}
{showHead === '1' && {showHead === '1' &&
(isPc ? ( (isPc ? (
!isPlugin && ( <ChatWindowHeader
<ChatWindowHeader title={chatWindowTitle}
title={chatWindowTitle} history={chatRecords}
history={chatRecords} chatType={ChatTypeEnum.chat}
chatType={ChatTypeEnum.share} rightActions={<SandboxEntryIcon onOpen={onOpenSandboxModal} />}
/> />
)
) : ( ) : (
<Flex <Flex
h="48px" h="48px"
...@@ -426,6 +443,7 @@ const OutLink = (props: Props) => { ...@@ -426,6 +443,7 @@ const OutLink = (props: Props) => {
/> />
)} )}
</Box> </Box>
<SandboxEditorModal />
</Flex> </Flex>
</Flex> </Flex>
</PageContainer> </PageContainer>
......
...@@ -13,6 +13,11 @@ import { getWebReqUrl } from '@fastgpt/web/common/system/utils'; ...@@ -13,6 +13,11 @@ import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
const splitMarker = 'SPLIT_MARKER'; const splitMarker = 'SPLIT_MARKER';
const contentType = 'audio/mpeg'; const contentType = 'audio/mpeg';
const isAbortError = (error: unknown) =>
error instanceof DOMException
? error.name === 'AbortError'
: error instanceof Error && error.name === 'AbortError';
// 添加 MediaSource 支持检测函数 // 添加 MediaSource 支持检测函数
const isMediaSourceSupported = () => { const isMediaSourceSupported = () => {
return typeof MediaSource !== 'undefined' && MediaSource.isTypeSupported?.(contentType); return typeof MediaSource !== 'undefined' && MediaSource.isTypeSupported?.(contentType);
...@@ -129,25 +134,31 @@ export const useAudioPlay = ( ...@@ -129,25 +134,31 @@ export const useAudioPlay = (
if (!isMediaSourceSupported()) { if (!isMediaSourceSupported()) {
// 不支持 MediaSource 时,直接读取完整流并播放 // 不支持 MediaSource 时,直接读取完整流并播放
return new Promise<Uint8Array>(async (resolve) => { return new Promise<Uint8Array>(async (resolve, reject) => {
const reader = stream.getReader(); const reader = stream.getReader();
const chunks: Uint8Array[] = []; const chunks: Uint8Array[] = [];
while (true) { try {
const { done, value } = await reader.read(); while (true) {
if (done) break; const { done, value } = await reader.read();
chunks.push(value); if (done) break;
} chunks.push(value);
}
const fullBuffer = new Uint8Array(chunks.reduce((acc, chunk) => acc + chunk.length, 0)); const fullBuffer = new Uint8Array(
let offset = 0; chunks.reduce((acc, chunk) => acc + chunk.length, 0)
for (const chunk of chunks) { );
fullBuffer.set(chunk, offset); let offset = 0;
offset += chunk.length; for (const chunk of chunks) {
} fullBuffer.set(chunk, offset);
offset += chunk.length;
}
playAudioBuffer(fullBuffer); playAudioBuffer(fullBuffer);
resolve(fullBuffer); resolve(fullBuffer);
} catch (error) {
reject(error);
}
}); });
} }
...@@ -219,6 +230,10 @@ export const useAudioPlay = ( ...@@ -219,6 +230,10 @@ export const useAudioPlay = (
resolve({}); resolve({});
} }
} catch (error) { } catch (error) {
if (isAbortError(error)) {
return resolve({});
}
toast({ toast({
status: 'error', status: 'error',
title: getErrText(error, t('common:core.chat.Audio Speech Error')) title: getErrText(error, t('common:core.chat.Audio Speech Error'))
......
...@@ -85,7 +85,31 @@ const ChatContextProvider = ({ ...@@ -85,7 +85,31 @@ const ChatContextProvider = ({
const forbidLoadChat = useRef(false); const forbidLoadChat = useRef(false);
const { chatId, setChatId, outLinkAuthData } = useChatStore(); const { chatId, setChatId, outLinkAuthData } = useChatStore();
const historyAppId = String(params.appId ?? ''); const historyAppId = typeof params.appId === 'string' ? params.appId : '';
const historySkillId = typeof params.skillId === 'string' ? params.skillId : '';
const historyTarget = useMemo(
() => ({
...(historyAppId ? { appId: historyAppId } : {}),
...(historySkillId ? { skillId: historySkillId } : {})
}),
[historyAppId, historySkillId]
);
const historyAuthData = useMemo(
() => ({
...outLinkAuthData,
...(typeof params.shareId === 'string' ? { shareId: params.shareId } : {}),
...(typeof params.outLinkUid === 'string' ? { outLinkUid: params.outLinkUid } : {}),
...(typeof params.teamId === 'string' ? { teamId: params.teamId } : {}),
...(typeof params.teamToken === 'string' ? { teamToken: params.teamToken } : {})
}),
[
outLinkAuthData,
params.outLinkUid,
params.shareId,
params.teamId,
params.teamToken
]
);
const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: openSlider } = useDisclosure(); const { isOpen: isOpenSlider, onClose: onCloseSlider, onOpen: openSlider } = useDisclosure();
const openSliderTimerRef = useRef<ReturnType<typeof setTimeout>>(); const openSliderTimerRef = useRef<ReturnType<typeof setTimeout>>();
...@@ -167,9 +191,9 @@ const ChatContextProvider = ({ ...@@ -167,9 +191,9 @@ const ChatContextProvider = ({
const { runAsync: onUpdateHistory } = useRequest( const { runAsync: onUpdateHistory } = useRequest(
(data: UpdateHistoryParams) => (data: UpdateHistoryParams) =>
putChatHistory({ putChatHistory({
appId: historyAppId, ...historyTarget,
...data, ...data,
...outLinkAuthData ...historyAuthData
}), }),
{ {
onBefore(params) { onBefore(params) {
...@@ -180,7 +204,7 @@ const ChatContextProvider = ({ ...@@ -180,7 +204,7 @@ const ChatContextProvider = ({
if (history.chatId === chatId) { if (history.chatId === chatId) {
return { return {
...history, ...history,
customTitle: customTitle || history.customTitle, customTitle: customTitle !== undefined ? customTitle : history.customTitle,
top: top !== undefined ? top : history.top top: top !== undefined ? top : history.top
}; };
} }
...@@ -192,7 +216,7 @@ const ChatContextProvider = ({ ...@@ -192,7 +216,7 @@ const ChatContextProvider = ({
: updatedHistories; : updatedHistories;
}); });
}, },
refreshDeps: [outLinkAuthData, historyAppId], refreshDeps: [historyAuthData, historyTarget],
errorToast: undefined errorToast: undefined
} }
); );
...@@ -200,9 +224,9 @@ const ChatContextProvider = ({ ...@@ -200,9 +224,9 @@ const ChatContextProvider = ({
const { runAsync: onDelHistory, loading: isDeletingHistory } = useRequest( const { runAsync: onDelHistory, loading: isDeletingHistory } = useRequest(
(chatId: string) => (chatId: string) =>
delChatHistoryById({ delChatHistoryById({
...(historyAppId ? { appId: historyAppId } : {}), ...historyTarget,
chatId, chatId,
...outLinkAuthData ...historyAuthData
}), }),
{ {
onSuccess(data, params) { onSuccess(data, params) {
...@@ -214,18 +238,18 @@ const ChatContextProvider = ({ ...@@ -214,18 +238,18 @@ const ChatContextProvider = ({
setHistoriesTotal((total) => Math.max(total - 1, 0)); setHistoriesTotal((total) => Math.max(total - 1, 0));
} }
}, },
refreshDeps: [outLinkAuthData, historyAppId] refreshDeps: [historyAuthData, historyTarget]
} }
); );
const { runAsync: onClearHistories, loading: isClearingHistory } = useRequest( const { runAsync: onClearHistories, loading: isClearingHistory } = useRequest(
() => () =>
delClearChatHistories({ delClearChatHistories({
...(historyAppId ? { appId: historyAppId } : {}), ...historyTarget,
...outLinkAuthData ...historyAuthData
}), }),
{ {
refreshDeps: [outLinkAuthData, historyAppId], refreshDeps: [historyAuthData, historyTarget],
onSuccess() { onSuccess() {
setHistories([]); setHistories([]);
setHistoriesTotal(0); setHistoriesTotal(0);
...@@ -305,9 +329,9 @@ const ChatContextProvider = ({ ...@@ -305,9 +329,9 @@ const ChatContextProvider = ({
const poll = () => { const poll = () => {
const chatIds = historiesRef.current.map((h) => h.chatId); const chatIds = historiesRef.current.map((h) => h.chatId);
getChatHistoryStatus({ getChatHistoryStatus({
...(historyAppId ? { appId: historyAppId } : {}), ...historyTarget,
chatIds, chatIds,
...outLinkAuthData ...historyAuthData
}) })
.then((res) => { .then((res) => {
const map = new Map(res.list.map((i) => [i.chatId, i])); const map = new Map(res.list.map((i) => [i.chatId, i]));
...@@ -356,7 +380,7 @@ const ChatContextProvider = ({ ...@@ -356,7 +380,7 @@ const ChatContextProvider = ({
window.clearInterval(timer); window.clearInterval(timer);
document.removeEventListener('visibilitychange', onVisibility); document.removeEventListener('visibilitychange', onVisibility);
}; };
}, [historyAppId, historyChatIdsKey, hasGeneratingInSidebar, outLinkAuthData, setHistories]); }, [historyTarget, historyChatIdsKey, hasGeneratingInSidebar, historyAuthData, setHistories]);
const isLoading = isDeletingHistory || isClearingHistory || isPaginationLoading; const isLoading = isDeletingHistory || isClearingHistory || isPaginationLoading;
......
...@@ -3,14 +3,17 @@ import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type'; ...@@ -3,14 +3,17 @@ import type { ChatHistoryItemType } from '@fastgpt/global/core/chat/type';
import i18next from 'i18next'; import i18next from 'i18next';
export const getDisplayHistoryTitle = ({ export const getDisplayHistoryTitle = ({
customTitle,
title, title,
fallbackTitle = i18next.t('common:core.chat.New Chat') fallbackTitle = i18next.t('common:core.chat.New Chat')
}: { }: {
customTitle?: string;
title?: string; title?: string;
fallbackTitle?: string; fallbackTitle?: string;
}) => { }) => {
const normalizedCustomTitle = customTitle?.trim();
const normalizedTitle = title?.trim(); const normalizedTitle = title?.trim();
return normalizedTitle || fallbackTitle; return normalizedCustomTitle || normalizedTitle || fallbackTitle;
}; };
export const normalizeHistoryTitle = (history: ChatHistoryItemType) => ({ export const normalizeHistoryTitle = (history: ChatHistoryItemType) => ({
......
...@@ -5,12 +5,14 @@ import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants'; ...@@ -5,12 +5,14 @@ import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools'; import { getNanoid } from '@fastgpt/global/common/string/tools';
import { MongoApp } from '@fastgpt/service/core/app/schema'; import { MongoApp } from '@fastgpt/service/core/app/schema';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema'; import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import { getUser } from '@test/datas/users'; import { getUser } from '@test/datas/users';
import { Call } from '@test/utils/request'; import { Call } from '@test/utils/request';
import { describe, expect, it, beforeEach } from 'vitest'; import { describe, expect, it, beforeEach } from 'vitest';
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema'; import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant';
import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant'; import { PerResourceTypeEnum } from '@fastgpt/global/support/permission/constant';
import { PublishChannelEnum } from '@fastgpt/global/support/outLink/constant';
describe('updateHistory api test', () => { describe('updateHistory api test', () => {
let testUser: Awaited<ReturnType<typeof getUser>>; let testUser: Awaited<ReturnType<typeof getUser>>;
...@@ -120,6 +122,51 @@ describe('updateHistory api test', () => { ...@@ -120,6 +122,51 @@ describe('updateHistory api test', () => {
expect(updatedChat?.top).toBe(true); expect(updatedChat?.top).toBe(true);
}); });
it('should update top status for share history without appId', async () => {
const shareId = `share-update-history-${getNanoid()}`;
const outLinkUid = `share-user-${getNanoid()}`;
await MongoOutLink.create({
shareId,
teamId: testUser.teamId,
tmbId: testUser.tmbId,
appId,
type: PublishChannelEnum.share,
name: 'Share Link'
});
await MongoChat.updateOne(
{ appId, chatId },
{
$set: {
shareId,
outLinkUid,
source: ChatSourceEnum.share
}
}
);
const res = await Call<UpdateHistoryBodyType, {}>(handler, {
body: {
shareId,
outLinkUid,
chatId,
top: true
}
});
expect(res.code).toBe(200);
expect(res.error).toBeUndefined();
const updatedChat = await MongoChat.findOne({
appId,
chatId,
shareId,
outLinkUid
});
expect(updatedChat?.top).toBe(true);
});
it('should update multiple fields at once', async () => { it('should update multiple fields at once', async () => {
const newTitle = 'New Title'; const newTitle = 'New Title';
const customTitle = 'New Custom Title'; const customTitle = 'New Custom Title';
......
...@@ -11,11 +11,6 @@ ...@@ -11,11 +11,6 @@
"cache": false, "cache": false,
"persistent": true "persistent": true
}, },
"dev:pro": {
"with": ["@fastgpt/admin#dev"],
"cache": false,
"persistent": true
},
"build": { "build": {
"dependsOn": ["^build"], "dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "worker/**"] "outputs": [".next/**", "dist/**", "worker/**"]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment