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
@@ -19,12 +19,14 @@
import { getClient } from '@hcengineering/presentation'
import { getResource } from '@hcengineering/platform'
import view, { Action as ViewAction } from '@hcengineering/view'
import { Ref } from '@hcengineering/core'
import ActivityMessageAction from './ActivityMessageAction.svelte'
import { savedMessagesStore } from '../activity'
export let message: ActivityMessage | undefined
export let actions: Action[] = []
export let excludedActions: Ref<ViewAction>[] = []
export let withActionMenu = true
export let onOpen: () => void
export let onClose: () => void
@@ -34,7 +36,7 @@
let inlineActions: ViewAction[] = []
let isActionMenuOpened = false
$: void updateInlineActions(message)
$: void updateInlineActions(message, excludedActions)
savedMessagesStore.subscribe(() => {
void updateInlineActions(message)
@@ -51,15 +53,13 @@
}
function showMenu (ev: MouseEvent): void {
const excludedActions = inlineActions.map(({ _id }) => _id)
showPopup(
Menu,
{
object: message,
actions,
baseMenuClass: activity.class.ActivityMessage,
excludedActions
excludedActions: inlineActions.map(({ _id }) => _id).concat(excludedActions)
},
ev.target as HTMLElement,
handleActionMenuClosed
@@ -67,14 +67,14 @@
handleActionMenuOpened()
}
async function updateInlineActions (message?: ActivityMessage): Promise<void> {
async function updateInlineActions (message?: ActivityMessage, excludedAction: Ref<ViewAction>[] = []): Promise<void> {
if (message === undefined) {
inlineActions = []
return
}
inlineActions = (await getActions(client, message, activity.class.ActivityMessage)).filter(
(action) => action.inline
)
inlineActions = (await getActions(client, message, activity.class.ActivityMessage))
.filter((action) => action.inline)
.filter((action) => !excludedAction.includes(action._id))
}
</script>
@@ -20,11 +20,12 @@
} from '@hcengineering/activity'
import { Person } from '@hcengineering/contact'
import { Avatar, EmployeePresenter, SystemAvatar } from '@hcengineering/contact-resources'
import core from '@hcengineering/core'
import core, { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Action, Icon, Label } from '@hcengineering/ui'
import { getActions, restrictionStore, showMenu } from '@hcengineering/view-resources'
import { Asset } from '@hcengineering/platform'
import { Action as ViewAction } from '@hcengineering/view'
import ReactionsPresenter from '../reactions/ReactionsPresenter.svelte'
import ActivityMessagePresenter from './ActivityMessagePresenter.svelte'
@@ -33,6 +34,8 @@
import { savedMessagesStore } from '../../activity'
import MessageTimestamp from '../MessageTimestamp.svelte'
import Replies from '../Replies.svelte'
import { MessageInlineAction } from '../../types'
import InlineAction from './InlineAction.svelte'
export let message: DisplayActivityMessage
export let parentMessage: DisplayActivityMessage | undefined = undefined
@@ -55,6 +58,8 @@
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let showDatePreposition = false
export let type: ActivityMessageViewType = 'default'
export let inlineActions: MessageInlineAction[] = []
export let excludedActions: Ref<ViewAction>[] = []
export let onClick: (() => void) | undefined = undefined
export let socialIcon: Asset | undefined = undefined
@@ -223,6 +228,14 @@
<span class="text-sm lower">
<MessageTimestamp date={message.createdOn ?? message.modifiedOn} />
</span>
{#if withActions && inlineActions.length > 0 && !readonly}
<div class="flex-presenter flex-gap-2 ml-2">
{#each inlineActions as item}
<InlineAction {item} />
{/each}
</div>
{/if}
</div>
{/if}
@@ -244,6 +257,7 @@
message={isReactionMessage(message) ? parentMessage : message}
{actions}
{withActionMenu}
{excludedActions}
onOpen={handleActionsOpened}
onClose={handleActionsClosed}
/>
@@ -0,0 +1,45 @@
<!--
// 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.
-->
<script lang="ts">
import { Label } from '@hcengineering/ui'
import { MessageInlineAction } from '../../types'
export let item: MessageInlineAction
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="root" class:clickable={item.onClick !== undefined} on:click={item.onClick}>
<Label label={item.label} />
</div>
<style lang="scss">
.root {
display: flex;
align-items: center;
font-size: 0.75rem;
color: var(--global-secondary-TextColor);
cursor: default;
&.clickable {
cursor: pointer;
&:hover {
color: var(--global-primary-TextColor);
}
}
}
</style>
+1
View File
@@ -41,6 +41,7 @@ import {
shouldScrollToActivity
} from './utils'
export * from './types'
export * from './activity'
export * from './utils'
export * from './activityMessagesUtils'
+20
View File
@@ -0,0 +1,20 @@
//
// 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 { type IntlString } from '@hcengineering/platform'
export interface MessageInlineAction {
label: IntlString
onClick?: () => Promise<void>
}
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname }
}
+22
View File
@@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120,
"useTabs": false,
"bracketSpacing": true,
"proseWrap": "preserve",
"plugins": [
"prettier-plugin-svelte"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "ui"
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)']
}
+47
View File
@@ -0,0 +1,47 @@
{
"name": "@hcengineering/ai-bot-resources",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile ui",
"build:docs": "api-extractor run --local",
"format": "format src",
"svelte-check": "do-svelte-check",
"_phase:svelte-check": "do-svelte-check",
"build:watch": "compile ui",
"_phase:build": "compile ui",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@types/jest": "^29.5.5",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint": "^8.54.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-svelte": "^2.35.1",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"prettier-plugin-svelte": "^3.2.2",
"sass": "^1.53.0",
"svelte-check": "^3.6.9",
"svelte-eslint-parser": "^0.33.1",
"svelte-loader": "^3.2.0",
"svelte-preprocess": "^5.1.3",
"ts-jest": "^29.1.1",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/ai-bot": "^0.6.0",
"@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/presentation": "^0.6.3",
"svelte": "^4.2.12"
}
}
@@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}
+20
View File
@@ -0,0 +1,20 @@
//
// 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 { type Resources } from '@hcengineering/platform'
export * from './utils'
export default async (): Promise<Resources> => ({})
+19
View File
@@ -0,0 +1,19 @@
//
// Copyright © 2024 Anticrm Platform Contributors.
//
// 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 { mergeIds } from '@hcengineering/platform'
import aiBot, { aiBotId } from '@hcengineering/ai-bot'
export default mergeIds(aiBotId, aiBot, {})
+49
View File
@@ -0,0 +1,49 @@
//
// 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 { concatLink, type Markup } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { type TranslateRequest, type TranslateResponse } from '@hcengineering/ai-bot'
import aiBot from './plugin'
export async function translate (text: Markup, lang: string): Promise<TranslateResponse | undefined> {
const url = getMetadata(aiBot.metadata.EndpointURL) ?? ''
const token = getMetadata(presentation.metadata.Token) ?? ''
if (url === '' || token === '') {
return undefined
}
try {
const req: TranslateRequest = { text, lang }
const resp = await fetch(concatLink(url, '/translate'), {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
})
if (!resp.ok) {
return undefined
}
return (await resp.json()) as TranslateResponse
} catch (error) {
console.error(error)
return undefined
}
}
@@ -0,0 +1,5 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess()
};
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types"
}
}
+6 -1
View File
@@ -14,10 +14,12 @@
//
import { Account, Class, Doc, type Mixin, Ref, Space } from '@hcengineering/core'
import type { Plugin } from '@hcengineering/platform'
import type { Metadata, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { ChatMessage } from '@hcengineering/chunter'
export * from './types'
export const aiBotId = 'ai-bot' as Plugin
export const aiBotAccountEmail = 'huly.ai.bot@hc.engineering'
@@ -51,6 +53,9 @@ export interface TransferredMessage extends ChatMessage {
}
const aiBot = plugin(aiBotId, {
metadata: {
EndpointURL: '' as Metadata<string>
},
class: {
AIBotEvent: '' as Ref<Class<AIBotEvent>>,
AIBotTransferEvent: '' as Ref<Class<AIBotTransferEvent>>,
+26
View File
@@ -0,0 +1,26 @@
//
// 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 { Markup } from '@hcengineering/core'
export interface TranslateRequest {
text: Markup
lang: string
}
export interface TranslateResponse {
text: Markup
lang: string
}
+5 -1
View File
@@ -122,6 +122,10 @@
"YouJoinedChannel": "You have been joined to channel",
"AndMore": "and {count} more",
"IsTyping": "{count, plural, =1 {is} other {are}} typing...",
"ThreadIn": "thread in {name}"
"ThreadIn": "thread in {name}",
"TranslateMessage": "Translate message",
"Translate": "Translate",
"ShowOriginal": "Show original",
"Translating": "Translating..."
}
}
+5 -1
View File
@@ -122,6 +122,10 @@
"YouJoinedChannel": "Te has unido al canal",
"AndMore": "y {count} más",
"IsTyping": "está escribiendo...",
"ThreadIn": "hilo en {name}"
"ThreadIn": "hilo en {name}",
"TranslateMessage": "Traducir mensaje",
"Translate": "Traducir",
"ShowOriginal": "Mostrar original",
"Translating": "Traduciendo..."
}
}
+5 -1
View File
@@ -122,6 +122,10 @@
"YouJoinedChannel": "Vous avez rejoint le canal",
"AndMore": "et {count} de plus",
"IsTyping": "est en train d'écrire...",
"ThreadIn": "fil de discussion dans {name}"
"ThreadIn": "fil de discussion dans {name}",
"TranslateMessage": "Traduire le message",
"Translate": "Traduire",
"ShowOriginal": "Afficher l'original",
"Translating": "Traduction en cours..."
}
}
+5 -1
View File
@@ -122,6 +122,10 @@
"YouJoinedChannel": "Entrou no canal",
"AndMore": "e mais {count}",
"IsTyping": "está a escrever...",
"ThreadIn": "Conversa em cadeia em {name}"
"ThreadIn": "Conversa em cadeia em {name}",
"TranslateMessage": "Traduzir mensagem",
"Translate": "Traduzir",
"ShowOriginal": "Mostrar original",
"Translating": "A traduzir..."
}
}
+5 -1
View File
@@ -122,6 +122,10 @@
"YouJoinedChannel": "Вы присоединились к каналу",
"AndMore": "и еще {count}",
"IsTyping": "{count, plural, =1 {печатает} other {печатают}}...",
"ThreadIn": "Обсуждение в {name}"
"ThreadIn": "Обсуждение в {name}",
"TranslateMessage": "Перевести сообщение",
"Translate": "Перевести",
"ShowOriginal": "Показать оригинал",
"Translating": "Перевод..."
}
}
+5 -1
View File
@@ -122,6 +122,10 @@
"YouJoinedChannel": "你已加入频道",
"AndMore": "和 {count} 人",
"IsTyping": "正在输入...",
"ThreadIn": "线程在 {name}"
"ThreadIn": "线程在 {name}",
"TranslateMessage": "翻译消息",
"Translate": "翻译",
"ShowOriginal": "显示原文",
"Translating": "翻译中..."
}
}
+4 -3
View File
@@ -39,9 +39,11 @@
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/activity-resources": "^0.6.1",
"@hcengineering/ai-bot": "^0.6.0",
"@hcengineering/ai-bot-resources": "^0.6.0",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/attachment": "^0.6.14",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/chunter": "^0.6.20",
@@ -61,7 +63,6 @@
"@hcengineering/workbench": "^0.6.16",
"@hcengineering/workbench-resources": "^0.6.1",
"fast-equals": "^5.0.1",
"svelte": "^4.2.12",
"html-to-text": "^9.0.3"
"svelte": "^4.2.12"
}
}
@@ -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 !== ''
}
+8 -1
View File
@@ -205,7 +205,11 @@ export default plugin(chunterId, {
YouJoinedChannel: '' as IntlString,
AndMore: '' as IntlString,
IsTyping: '' as IntlString,
ThreadIn: '' as IntlString
ThreadIn: '' as IntlString,
TranslateMessage: '' as IntlString,
Translate: '' as IntlString,
ShowOriginal: '' as IntlString,
Translating: '' as IntlString
},
ids: {
DMNotification: '' as Ref<NotificationType>,
@@ -222,9 +226,12 @@ export default plugin(chunterId, {
DeleteChatMessage: '' as Ref<Action>,
LeaveChannel: '' as Ref<Action>,
RemoveChannel: '' as Ref<Action>,
TranslateMessage: '' as Ref<Action>,
ShowOriginalMessage: '' as Ref<Action>,
CloseConversation: '' as Ref<Action>
},
function: {
CanTranslateMessage: '' as Resource<(doc?: Doc | Doc[]) => Promise<boolean>>,
OpenChannelInSidebar: '' as Resource<
(_id: Ref<Doc>, _class: Ref<Doc>, doc?: Doc, thread?: Ref<ActivityMessage>) => Promise<void>
>
+1 -1
View File
@@ -42,6 +42,6 @@
"@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11",
"openai": "^4.56.0",
"tiktoken": "^1.0.16"
"js-tiktoken": "^1.0.14"
}
}
+1 -1
View File
@@ -14,7 +14,7 @@
//
import OpenAI from 'openai'
import { Tiktoken } from 'tiktoken'
import { Tiktoken } from 'js-tiktoken'
// Based on https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
export function countTokens (messages: OpenAI.ChatCompletionMessageParam[], encoding: Tiktoken): number {
+13
View File
@@ -183,4 +183,17 @@
<symbol id="details-filled" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 8.00049C2 5.79135 3.79086 4.00049 6 4.00049H26C28.2091 4.00049 30 5.79135 30 8.00049V24.0005C30 26.2096 28.2091 28.0005 26 28.0005H6C3.79086 28.0005 2 26.2096 2 24.0005V8.00049ZM20 6.00049H6C4.89543 6.00049 4 6.89592 4 8.00049V24.0005C4 25.1051 4.89543 26.0005 6 26.0005H20V6.00049Z"/>
</symbol>
<symbol id="translate" viewBox="0 0 32 32">
<path d="M17,8V3a2,2,0,0,0-2-2H3A2,2,0,0,0,1,3V14a2,2,0,0,0,2,2H4v2.667L8,16h3" fill="none" stroke="currentColor"
stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"/>
<path d="M15,25V14a2,2,0,0,1,2-2H29a2,2,0,0,1,2,2V25a2,2,0,0,1-2,2H28v2.667L24,27H17A2,2,0,0,1,15,25Z"
fill="none" stroke="currentColor" stroke-linecap="square" stroke-miterlimit="10" stroke-width="2"/>
<path d="M24.825,24,24.4,22.4H21.62L21.187,24H18.644l2.794-8.6h3.088l2.83,8.6Zm-.9-3.5-.369-1.406q-.129-.469-.314-1.213T23,16.811q-.053.3-.208.984T22.1,20.5Z"
fill="currentColor" stroke="none"/>
<path d="M13.685,6.453c-.44-.04-.83-.06-1.419-.06a7.85,7.85,0,0,1-.68,1.929,7.4,7.4,0,0,1-1.109,1.619,9.6,9.6,0,0,0,3.428,1.209,6.426,6.426,0,0,0-.99,1.839A12.35,12.35,0,0,1,9.107,11.17a12.776,12.776,0,0,1-3.978,1.839A5.793,5.793,0,0,0,4.17,11.24,10.869,10.869,0,0,0,7.718,9.931,8.7,8.7,0,0,1,6.429,8.1a8.77,8.77,0,0,1-.69-1.709c-.59,0-.929.02-1.429.06V4.644a12.321,12.321,0,0,0,1.449.07H7.968v-.6a3.017,3.017,0,0,0-.06-.619H9.917a3.068,3.068,0,0,0-.06.609v.61h2.378a12.226,12.226,0,0,0,1.45-.07Zm-6.027-.06a5.7,5.7,0,0,0,1.4,2.419,5.645,5.645,0,0,0,1.25-2.419Z"
fill="currentColor" stroke="none"/>
</symbol>
<symbol id="undo" viewBox="0 0 32 32">
<path d="M20 10H7.8149L11.4023 6.4141L10 5L4 11L10 17L11.4023 15.5854L7.8179 12H20C21.5913 12 23.1174 12.6321 24.2426 13.7574C25.3679 14.8826 26 16.4087 26 18C26 19.5913 25.3679 21.1174 24.2426 22.2426C23.1174 23.3679 21.5913 24 20 24H13C12.4477 24 12 24.4477 12 25C12 25.5523 12.4477 26 13 26H20C22.1217 26 24.1566 25.1571 25.6569 23.6569C27.1571 22.1566 28 20.1217 28 18C28 15.8783 27.1571 13.8434 25.6569 12.3431C24.1566 10.8429 22.1217 10 20 10Z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

+3 -1
View File
@@ -56,5 +56,7 @@ loadMetadata(view.icon, {
Join: `${icons}#join`,
Leave: `${icons}#leave`,
Copy: `${icons}#copy`,
DetailsFilled: `${icons}#details-filled`
DetailsFilled: `${icons}#details-filled`,
Translate: `${icons}#translate`,
Undo: `${icons}#undo`
})
+3 -1
View File
@@ -252,7 +252,9 @@ const view = plugin(viewId, {
Leave: '' as Asset,
Copy: '' as Asset,
TodoList: '' as Asset,
DetailsFilled: '' as Asset
DetailsFilled: '' as Asset,
Translate: '' as Asset,
Undo: '' as Asset
},
category: {
General: '' as Ref<ActionCategory>,