Add translate message action (#6609)

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina
2024-09-19 22:35:18 +07:00
committed by GitHub
parent c0448a373f
commit 8d9cebdd67
62 changed files with 926 additions and 130 deletions
@@ -15,21 +15,22 @@
<script lang="ts">
import contact, { Person, PersonAccount } from '@hcengineering/contact'
import { personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources'
import { Class, Doc, getCurrentAccount, Ref, Space, WithLookup } from '@hcengineering/core'
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import { Class, Doc, getCurrentAccount, Markup, Ref, Space, WithLookup } from '@hcengineering/core'
import { getClient, MessageViewer } from '@hcengineering/presentation'
import { AttachmentDocList, AttachmentImageSize } from '@hcengineering/attachment-resources'
import { getDocLinkTitle } from '@hcengineering/view-resources'
import { Action, Button, IconEdit, ShowMore } from '@hcengineering/ui'
import view from '@hcengineering/view'
import activity, { ActivityMessage, ActivityMessageViewType, DisplayActivityMessage } from '@hcengineering/activity'
import { ActivityDocLink, ActivityMessageTemplate } from '@hcengineering/activity-resources'
import { ActivityDocLink, ActivityMessageTemplate, MessageInlineAction } from '@hcengineering/activity-resources'
import chunter, { ChatMessage, ChatMessageViewlet, InlineButton } from '@hcengineering/chunter'
import { Attachment } from '@hcengineering/attachment'
import { getMetadata } from '@hcengineering/platform'
import { EmptyMarkup } from '@hcengineering/text'
import ChatMessageHeader from './ChatMessageHeader.svelte'
import ChatMessageInput from './ChatMessageInput.svelte'
import InlineButtons from '../InlineButtons.svelte'
import { translatedMessagesStore, translatingMessagesStore, shownTranslatedMessagesStore } from '../../stores'
export let value: WithLookup<ChatMessage> | undefined
export let doc: Doc | undefined = undefined
@@ -81,6 +82,26 @@
$: account = accountId !== undefined ? $personAccountByIdStore.get(accountId as Ref<PersonAccount>) : undefined
$: person = account?.person !== undefined ? $personByIdStore.get(account.person) : undefined
let originalText = value?.message
$: if (value && originalText && originalText !== value?.message) {
originalText = value.message
translatedMessagesStore.update((map) => {
if (value) {
map.delete(value._id)
}
return map
})
shownTranslatedMessagesStore.update((map) => {
if (value) {
map.delete(value._id)
}
return map
})
} else {
originalText = value?.message
}
$: value !== undefined &&
getParentMessage(value.attachedToClass, value.attachedTo, value.space).then((res) => {
parentMessage = res as DisplayActivityMessage
@@ -147,9 +168,52 @@
let inlineButtons: InlineButton[] = []
$: inlineButtons = (value?.$lookup?.inlineButtons ?? []) as InlineButton[]
let inlineActions: MessageInlineAction[] = []
$: updateInlineActions($translatingMessagesStore, $shownTranslatedMessagesStore)
function updateInlineActions (
translatingMessages: Set<Ref<ChatMessage>>,
shownTranslated: Set<Ref<ChatMessage>>
): void {
if (value === undefined) {
inlineActions = []
return
}
const result: MessageInlineAction[] = []
if (translatingMessages.has(value._id)) {
result.push({
label: chunter.string.Translating
})
} else if (shownTranslated.has(value._id)) {
result.push({
label: chunter.string.ShowOriginal,
onClick: async () => {
shownTranslatedMessagesStore.update((map) => {
if (value) {
map.delete(value._id)
}
return map
})
}
})
}
inlineActions = result
}
$: socialProvider = value?.provider
? client.getModel().findAllSync(contact.class.ChannelProvider, { _id: value.provider })[0]
: undefined
let displayText: Markup = value?.message ?? EmptyMarkup
$: if (value && $shownTranslatedMessagesStore.has(value._id)) {
displayText = $translatedMessagesStore.get(value._id) ?? value?.message ?? EmptyMarkup
} else {
displayText = value?.message ?? EmptyMarkup
}
</script>
{#if inline && object}
@@ -181,8 +245,12 @@
{skipLabel}
{pending}
{stale}
excludedActions={$shownTranslatedMessagesStore.has(value._id)
? [chunter.action.TranslateMessage]
: [chunter.action.ShowOriginalMessage]}
socialIcon={socialProvider?.icon}
showDatePreposition={hideLink}
{inlineActions}
{type}
{onClick}
>
@@ -194,7 +262,7 @@
{#if withShowMore}
<ShowMore limit={compact ? 80 : undefined}>
<div class="clear-mins">
<MessageViewer message={value.message} />
<MessageViewer message={displayText} />
{#if (value.attachments ?? 0) > 0 || (value.inlineButtons ?? 0) > 0}
<div class="mt-2" />
{/if}
@@ -204,7 +272,7 @@
</ShowMore>
{:else}
<div class="clear-mins">
<MessageViewer message={value.message} />
<MessageViewer message={displayText} />
{#if (value.attachments ?? 0) > 0 || (value.inlineButtons ?? 0) > 0}
<div class="mt-2" />
{/if}
@@ -19,10 +19,9 @@
import attachment, { Attachment } from '@hcengineering/attachment'
import { AttachmentsTooltip } from '@hcengineering/attachment-resources'
import { ChatMessage } from '@hcengineering/chunter'
import core from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Action, Icon, Label, tooltip } from '@hcengineering/ui'
import { convert } from 'html-to-text'
import { isEmptyMarkup } from '@hcengineering/text'
export let value: ChatMessage
export let readonly = false
@@ -46,22 +45,15 @@
} else {
attachmentsQuery.unsubscribe()
}
$: text = value.message
? convert(value.message, {
preserveNewlines: false,
selectors: [{ selector: 'img', format: 'skip' }]
})
: undefined
</script>
<BaseMessagePreview text={text ? value.message : undefined} message={value} {type} {readonly} {actions} on:click>
{#if value.attachments && type === 'full' && text}
<BaseMessagePreview text={value.message} message={value} {type} {readonly} {actions} on:click>
{#if value.attachments && type === 'full' && !isEmptyMarkup(value.message)}
<div class="attachments" use:tooltip={{ component: AttachmentsTooltip, props: { attachments } }}>
{value.attachments}
<Icon icon={attachment.icon.Attachment} size="small" />
</div>
{:else if attachments.length > 0 && !text}
{:else if attachments.length > 0 && isEmptyMarkup(value.message)}
<span class="font-normal">
<Label label={attachment.string.Attachments} />:
<span class="ml-1">
+9 -3
View File
@@ -75,7 +75,10 @@ import {
getTitle,
getUnreadThreadsCount,
leaveChannelAction,
removeChannelAction
removeChannelAction,
translateMessage,
showOriginalMessage,
canTranslateMessage
} from './utils'
export { default as ChatMessageInput } from './components/chat-message/ChatMessageInput.svelte'
@@ -196,7 +199,8 @@ export default async (): Promise<Resources> => ({
CanReplyToThread: canReplyToThread,
GetMessageLink: getMessageLocation,
CloseChatWidgetTab: closeChatWidgetTab,
OpenChannelInSidebar: openChannelInSidebar
OpenChannelInSidebar: openChannelInSidebar,
CanTranslateMessage: canTranslateMessage
},
actionImpl: {
ArchiveChannel,
@@ -207,6 +211,8 @@ export default async (): Promise<Resources> => ({
RemoveChannel: removeChannelAction,
ReplyToThread: replyToThread,
OpenInSidebar: openChannelInSidebarAction,
OpenInSidebarTab: openChannelInSidebarTabAction
OpenInSidebarTab: openChannelInSidebarTabAction,
TranslateMessage: translateMessage,
ShowOriginalMessage: showOriginalMessage
}
})
+28
View File
@@ -0,0 +1,28 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { writable } from 'svelte/store'
import { type ChatMessage } from '@hcengineering/chunter'
import { type Markup, type Ref } from '@hcengineering/core'
import { languageStore } from '@hcengineering/ui'
export const translatingMessagesStore = writable<Set<Ref<ChatMessage>>>(new Set())
export const translatedMessagesStore = writable<Map<Ref<ChatMessage>, Markup>>(new Map())
export const shownTranslatedMessagesStore = writable<Set<Ref<ChatMessage>>>(new Set())
languageStore.subscribe(() => {
translatedMessagesStore.set(new Map())
shownTranslatedMessagesStore.set(new Set())
})
+41 -2
View File
@@ -41,16 +41,19 @@ import {
isActivityNotification,
isMentionNotification
} from '@hcengineering/notification-resources'
import { translate, type Asset } from '@hcengineering/platform'
import { translate, type Asset, getMetadata } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import { type AnySvelteComponent } from '@hcengineering/ui'
import { type AnySvelteComponent, languageStore } from '@hcengineering/ui'
import { classIcon, getDocLinkTitle, getDocTitle } from '@hcengineering/view-resources'
import { get, writable, type Unsubscriber } from 'svelte/store'
import aiBot from '@hcengineering/ai-bot'
import { translate as aiTranslate } from '@hcengineering/ai-bot-resources'
import ChannelIcon from './components/ChannelIcon.svelte'
import DirectIcon from './components/DirectIcon.svelte'
import { resetChunterLocIfEqual } from './navigation'
import chunter from './plugin'
import { shownTranslatedMessagesStore, translatedMessagesStore, translatingMessagesStore } from './stores'
export async function getDmName (client: Client, space?: Space): Promise<string> {
if (space === undefined) {
@@ -528,3 +531,39 @@ export function isThreadMessage (message: ActivityMessage): message is ThreadMes
export function getChannelSpace (_class: Ref<Class<Doc>>, _id: Ref<Doc>, space: Ref<Space>): Ref<Space> {
return getClient().getHierarchy().isDerived(_class, core.class.Space) ? (_id as Ref<Space>) : space
}
export async function translateMessage (message: ChatMessage): Promise<void> {
if (get(translatingMessagesStore).has(message._id)) {
return
}
if (get(translatedMessagesStore).has(message._id)) {
shownTranslatedMessagesStore.update((store) => store.add(message._id))
return
}
translatingMessagesStore.update((store) => store.add(message._id))
const response = await aiTranslate(message.message, get(languageStore))
if (response !== undefined) {
translatedMessagesStore.update((store) => store.set(message._id, response.text))
shownTranslatedMessagesStore.update((store) => store.add(message._id))
}
translatingMessagesStore.update((store) => {
store.delete(message._id)
return store
})
}
export async function showOriginalMessage (message: ChatMessage): Promise<void> {
shownTranslatedMessagesStore.update((store) => {
store.delete(message._id)
return store
})
}
export async function canTranslateMessage (): Promise<boolean> {
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
return url !== ''
}