diff --git a/.github/workflows/i18n-unused-keys.yml b/.github/workflows/i18n-unused-keys.yml index 6bcf824946..07cc77a1ae 100644 --- a/.github/workflows/i18n-unused-keys.yml +++ b/.github/workflows/i18n-unused-keys.yml @@ -5,12 +5,13 @@ on: paths: - "client/src/**" - "api/**" + - "packages/data-provider/src/**" jobs: detect-unused-i18n-keys: runs-on: ubuntu-latest permissions: - pull-requests: write # Required for posting PR comments + pull-requests: write steps: - name: Checkout repository uses: actions/checkout@v3 diff --git a/api/app/clients/ChatGPTClient.js b/api/app/clients/ChatGPTClient.js index 07b2fa97bb..36a3f4936a 100644 --- a/api/app/clients/ChatGPTClient.js +++ b/api/app/clients/ChatGPTClient.js @@ -244,9 +244,9 @@ class ChatGPTClient extends BaseClient { baseURL = this.langchainProxy ? constructAzureURL({ - baseURL: this.langchainProxy, - azureOptions: this.azure, - }) + baseURL: this.langchainProxy, + azureOptions: this.azure, + }) : this.azureEndpoint.split(/(? { try { let done = false; diff --git a/api/app/clients/GoogleClient.js b/api/app/clients/GoogleClient.js index c9102e9ae2..4151e6663a 100644 --- a/api/app/clients/GoogleClient.js +++ b/api/app/clients/GoogleClient.js @@ -236,11 +236,11 @@ class GoogleClient extends BaseClient { msg.content = ( !Array.isArray(msg.content) ? [ - { - type: ContentTypes.TEXT, - [ContentTypes.TEXT]: msg.content, - }, - ] + { + type: ContentTypes.TEXT, + [ContentTypes.TEXT]: msg.content, + }, + ] : msg.content ).concat(message.image_urls); diff --git a/api/app/clients/specs/BaseClient.test.js b/api/app/clients/specs/BaseClient.test.js index d620d5f647..1cd4a80db7 100644 --- a/api/app/clients/specs/BaseClient.test.js +++ b/api/app/clients/specs/BaseClient.test.js @@ -52,7 +52,7 @@ const messageHistory = [ { role: 'user', isCreatedByUser: true, - text: 'What\'s up', + text: "What's up", messageId: '3', parentMessageId: '2', }, @@ -456,7 +456,7 @@ describe('BaseClient', () => { const chatMessages2 = await TestClient.loadHistory(conversationId, '3'); expect(TestClient.currentMessages).toHaveLength(3); - expect(chatMessages2[chatMessages2.length - 1].text).toEqual('What\'s up'); + expect(chatMessages2[chatMessages2.length - 1].text).toEqual("What's up"); }); /* Most of the new sendMessage logic revolving around edited/continued AI messages diff --git a/api/app/clients/specs/OpenAIClient.test.js b/api/app/clients/specs/OpenAIClient.test.js index 579f636eef..8e470727da 100644 --- a/api/app/clients/specs/OpenAIClient.test.js +++ b/api/app/clients/specs/OpenAIClient.test.js @@ -462,17 +462,17 @@ describe('OpenAIClient', () => { role: 'system', name: 'example_user', content: - 'Let\'s circle back when we have more bandwidth to touch base on opportunities for increased leverage.', + "Let's circle back when we have more bandwidth to touch base on opportunities for increased leverage.", }, { role: 'system', name: 'example_assistant', - content: 'Let\'s talk later when we\'re less busy about how to do better.', + content: "Let's talk later when we're less busy about how to do better.", }, { role: 'user', content: - 'This late pivot means we don\'t have time to boil the ocean for the client deliverable.', + "This late pivot means we don't have time to boil the ocean for the client deliverable.", }, ]; diff --git a/api/app/clients/tools/structured/OpenAIImageTools.js b/api/app/clients/tools/structured/OpenAIImageTools.js index afea9dfd55..499cda3ea7 100644 --- a/api/app/clients/tools/structured/OpenAIImageTools.js +++ b/api/app/clients/tools/structured/OpenAIImageTools.js @@ -64,7 +64,7 @@ const DEFAULT_IMAGE_EDIT_PROMPT_DESCRIPTION = `Describe the changes, enhancement Always base this prompt on the most recently uploaded reference images.`; const displayMessage = - 'The tool displayed an image. All generated images are already plainly visible, so don\'t repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user.'; + "The tool displayed an image. All generated images are already plainly visible, so don't repeat the descriptions in detail. Do not list download links as they are available in the UI already. The user may download the images by clicking on them, but do not mention anything about downloading to the user."; /** * Replaces unwanted characters from the input string diff --git a/api/models/Message.js b/api/models/Message.js index 86fd2fd549..9384c35f72 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -255,6 +255,7 @@ async function updateMessage(req, message, metadata) { text: updatedMessage.text, isCreatedByUser: updatedMessage.isCreatedByUser, tokenCount: updatedMessage.tokenCount, + feedback: updatedMessage.feedback, }; } catch (err) { logger.error('Error updating message:', err); diff --git a/api/models/Message.spec.js b/api/models/Message.spec.js index a542130b59..7aef843678 100644 --- a/api/models/Message.spec.js +++ b/api/models/Message.spec.js @@ -153,7 +153,7 @@ describe('Message Operations', () => { }); describe('Conversation Hijacking Prevention', () => { - it('should not allow editing a message in another user\'s conversation', async () => { + it("should not allow editing a message in another user's conversation", async () => { const attackerReq = { user: { id: 'attacker123' } }; const victimConversationId = 'victim-convo-123'; const victimMessageId = 'victim-msg-123'; @@ -175,7 +175,7 @@ describe('Message Operations', () => { ); }); - it('should not allow deleting messages from another user\'s conversation', async () => { + it("should not allow deleting messages from another user's conversation", async () => { const attackerReq = { user: { id: 'attacker123' } }; const victimConversationId = 'victim-convo-123'; const victimMessageId = 'victim-msg-123'; @@ -193,7 +193,7 @@ describe('Message Operations', () => { }); }); - it('should not allow inserting a new message into another user\'s conversation', async () => { + it("should not allow inserting a new message into another user's conversation", async () => { const attackerReq = { user: { id: 'attacker123' } }; const victimConversationId = uuidv4(); // Use a valid UUID diff --git a/api/server/controllers/agents/request.js b/api/server/controllers/agents/request.js index fcee62edc7..24b7822c1f 100644 --- a/api/server/controllers/agents/request.js +++ b/api/server/controllers/agents/request.js @@ -228,7 +228,7 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => { // Save user message if needed if (!client.skipSaveUserMessage) { await saveMessage(req, userMessage, { - context: 'api/server/controllers/agents/request.js - don\'t skip saving user message', + context: "api/server/controllers/agents/request.js - don't skip saving user message", }); } diff --git a/api/server/middleware/abortMiddleware.js b/api/server/middleware/abortMiddleware.js index bfc28f513d..94d69004bd 100644 --- a/api/server/middleware/abortMiddleware.js +++ b/api/server/middleware/abortMiddleware.js @@ -327,7 +327,7 @@ const handleAbortError = async (res, req, error, data) => { errorText = `{"type":"${ErrorTypes.INVALID_REQUEST}"}`; } - if (error?.message?.includes('does not support \'system\'')) { + if (error?.message?.includes("does not support 'system'")) { errorText = `{"type":"${ErrorTypes.NO_SYSTEM_MESSAGES}"}`; } diff --git a/api/server/routes/messages.js b/api/server/routes/messages.js index d5980ae55b..7f771a4821 100644 --- a/api/server/routes/messages.js +++ b/api/server/routes/messages.js @@ -253,6 +253,31 @@ router.put('/:conversationId/:messageId', validateMessageReq, async (req, res) = } }); +router.put('/:conversationId/:messageId/feedback', validateMessageReq, async (req, res) => { + try { + const { conversationId, messageId } = req.params; + const { feedback } = req.body; + + const updatedMessage = await updateMessage( + req, + { + messageId, + feedback: feedback || null, + }, + { context: 'updateFeedback' }, + ); + + res.json({ + messageId, + conversationId, + feedback: updatedMessage.feedback, + }); + } catch (error) { + logger.error('Error updating message feedback:', error); + res.status(500).json({ error: 'Failed to update feedback' }); + } +}); + router.delete('/:conversationId/:messageId', validateMessageReq, async (req, res) => { try { const { messageId } = req.params; diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 6837869e8e..0ac6387c33 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -457,11 +457,20 @@ export type VoiceOption = { }; export type TMessageAudio = { - messageId?: string; - content?: t.TMessageContentParts[] | string; - className?: string; - isLast: boolean; + isLast?: boolean; index: number; + messageId: string; + content: string; + className?: string; + renderButton?: (props: { + onClick: (e?: React.MouseEvent) => void; + title: string; + icon: React.ReactNode; + isActive?: boolean; + isVisible?: boolean; + isDisabled?: boolean; + className?: string; + }) => React.ReactNode; }; export type OptionWithIcon = Option & { icon?: React.ReactNode }; diff --git a/client/src/components/Audio/TTS.tsx b/client/src/components/Audio/TTS.tsx index 3ceacb7f8d..d5fcb91120 100644 --- a/client/src/components/Audio/TTS.tsx +++ b/client/src/components/Audio/TTS.tsx @@ -1,5 +1,5 @@ /* eslint-disable jsx-a11y/media-has-caption */ -import { useEffect, useMemo } from 'react'; +import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import type { TMessageAudio } from '~/common'; import { useLocalize, useTTSBrowser, useTTSExternal } from '~/hooks'; @@ -7,7 +7,14 @@ import { VolumeIcon, VolumeMuteIcon, Spinner } from '~/components'; import { logger } from '~/utils'; import store from '~/store'; -export function BrowserTTS({ isLast, index, messageId, content, className }: TMessageAudio) { +export function BrowserTTS({ + isLast, + index, + messageId, + content, + className, + renderButton, +}: TMessageAudio) { const localize = useLocalize(); const playbackRate = useRecoilValue(store.playbackRate); @@ -46,21 +53,30 @@ export function BrowserTTS({ isLast, index, messageId, content, className }: TMe audioRef.current, ); + const handleClick = () => { + if (audioRef.current) { + audioRef.current.muted = false; + } + toggleSpeech(); + }; + + const title = isSpeaking === true ? localize('com_ui_stop') : localize('com_ui_read_aloud'); + return ( <> - + {renderButton ? ( + renderButton({ + onClick: handleClick, + title: title, + icon: renderIcon('19'), + isActive: isSpeaking, + className, + }) + ) : ( + + )}