diff --git a/.github/workflows/i18n-unused-keys.yml b/.github/workflows/i18n-unused-keys.yml index 6bcf82494..07cc77a1a 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 07b2fa97b..36a3f4936 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 c9102e9ae..4151e6663 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 d620d5f64..1cd4a80db 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 579f636ee..8e470727d 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 afea9dfd5..499cda3ea 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 86fd2fd54..9384c35f7 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 a542130b5..7aef84367 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 fcee62edc..24b7822c1 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 bfc28f513..94d69004b 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 d5980ae55..7f771a482 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 6837869e8..0ac6387c3 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 3ceacb7f8..d5fcb9112 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, + }) + ) : ( + + )}