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
+1 -1
View File
@@ -458,7 +458,7 @@
"env": {
"ACCOUNTS_URL": "http://localhost:3000",
"MONGO_URL": "mongodb://localhost:27017",
"PORT": "4008",
"PORT": "4010",
"SERVER_SECRET": "secret",
"SUPPORT_WORKSPACE": "support",
"FIRST_NAME": "Jolie",
+67 -13
View File
@@ -74,6 +74,9 @@ dependencies:
'@rush-temp/ai-bot':
specifier: file:./projects/ai-bot.tgz
version: file:projects/ai-bot.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
'@rush-temp/ai-bot-resources':
specifier: file:./projects/ai-bot-resources.tgz
version: file:projects/ai-bot-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
'@rush-temp/analytics':
specifier: file:./projects/analytics.tgz
version: file:projects/analytics.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
@@ -1820,9 +1823,6 @@ dependencies:
telegram:
specifier: 2.22.2
version: 2.22.2
tiktoken:
specifier: ^1.0.16
version: 1.0.16
toposort:
specifier: ^2.0.2
version: 2.0.2
@@ -17077,6 +17077,12 @@ packages:
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
dev: false
/js-tiktoken@1.0.14:
resolution: {integrity: sha512-Pk3l3WOgM9joguZY2k52+jH82RtABRgB5RdGFZNUGbOKGMVlNmafcPA3b0ITcCZPu1L9UclP1tne6aw7ZI4Myg==}
dependencies:
base64-js: 1.5.1
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: false
@@ -24527,6 +24533,51 @@ packages:
- ts-node
dev: false
file:projects/ai-bot-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-kUT09zDslxlN/ni38NdRvzE4WtOBOtKbqKqTNvTDP78hcjh7rT3CnWpMUQGnDNI5rzQWiVAaWoHKf2LAHFolRQ==, tarball: file:projects/ai-bot-resources.tgz}
id: file:projects/ai-bot-resources.tgz
name: '@rush-temp/ai-bot-resources'
version: 0.0.0
dependencies:
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.3.3)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.12)(ts-node@10.9.2)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
sass: 1.71.1
svelte: 4.2.12
svelte-check: 3.6.9(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)
svelte-eslint-parser: 0.33.1(svelte@4.2.12)
svelte-loader: 3.2.0(svelte@4.2.12)
svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.12)(typescript@5.3.3)
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- coffeescript
- esbuild
- less
- node-notifier
- postcss
- postcss-load-config
- pug
- stylus
- sugarss
- supports-color
- ts-node
dev: false
file:projects/ai-bot.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-TH/kDh1LKw+pJRxLZZIjR7da6BQslR9S9Uy9HHXDtvtu5es55OmjRZEWPUnkBjKRmY6hTPBmaO4veq7RiaNz7g==, tarball: file:projects/ai-bot.tgz}
id: file:projects/ai-bot.tgz
@@ -25328,7 +25379,7 @@ packages:
dev: false
file:projects/chunter-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-RWD09VQ42IkJmhjvrQ6cqQ3etpChxFKhZICAQ6cuSbjetHSz0LKXSzrsE5xjpnsZE6Wr22dSDiglk9+82SVC8w==, tarball: file:projects/chunter-resources.tgz}
resolution: {integrity: sha512-qC9oDROK1wvmwya82IbB9HAj4bYw3oBPa23nMIW8phhnkSp+6fCVboz3D8G1EIw5eZmDbPCSgtnZZh+mH/VqLw==, tarball: file:projects/chunter-resources.tgz}
id: file:projects/chunter-resources.tgz
name: '@rush-temp/chunter-resources'
version: 0.0.0
@@ -25376,7 +25427,7 @@ packages:
dev: false
file:projects/chunter.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-7CCSMrDbX8Mb8drPtpIVbQbvx1fsXW6YAqceu/rhsE7b9+i3LRaNGlyggJ1V1Kg32FTEHK2Q4jkKrn34aLL+3A==, tarball: file:projects/chunter.tgz}
resolution: {integrity: sha512-30IiBYbNohYn7c6fiy/QTHMy6qkpRv3ZvghD595O/is4lFkDfaBLP18Zd90fgyQNg/G/2HtGMGXunlWEr/OAoQ==, tarball: file:projects/chunter.tgz}
id: file:projects/chunter.tgz
name: '@rush-temp/chunter'
version: 0.0.0
@@ -25535,7 +25586,7 @@ packages:
dev: false
file:projects/collaborator.tgz(@tiptap/pm@2.6.6)(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-protocols@1.0.6):
resolution: {integrity: sha512-ILQCgoJM6kiRT3YNphnywB42nFA5VZJkRXae0PiKrm+RJwm9/Qf0Berkrj+SDy32iC9fw6PI/+yES11MeqFeSA==, tarball: file:projects/collaborator.tgz}
resolution: {integrity: sha512-ljGlQulKneGos0pwJDjmKkgFZVizNJ4CMyGUp14orVTc40IKNK4szU09WskcwoZ6MGbBnUJttMVuGf3tb0/0iA==, tarball: file:projects/collaborator.tgz}
id: file:projects/collaborator.tgz
name: '@rush-temp/collaborator'
version: 0.0.0
@@ -26016,7 +26067,7 @@ packages:
dev: false
file:projects/desktop.tgz(bufferutil@4.0.8)(sass@1.71.1)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-W8ImBT7qeJoODsDzoy5ioDFr7A1YJX5S9xa8haLtNxiOce9iZKCyesZ5qK81g5ZgNETfhWXNo1+7u0fCcoG2qw==, tarball: file:projects/desktop.tgz}
resolution: {integrity: sha512-y8p1AOK7K4z/Y7hzkEy4MfAc3JNlLrR+54QYdqAmzCzF7NtuM4x9YeB7oQTmQfKYSmZHXJb4++pJ0+AFONIXSQ==, tarball: file:projects/desktop.tgz}
id: file:projects/desktop.tgz
name: '@rush-temp/desktop'
version: 0.0.0
@@ -27495,7 +27546,7 @@ packages:
dev: false
file:projects/love-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-e49VrhVkk6U8nj/ZyKfYVj/gK/3rTSrVCicDM5Luxw/f/8pPEVf9uEzmdAV11uiXuXxmj5t0BmHdG5eeVhiK6A==, tarball: file:projects/love-resources.tgz}
resolution: {integrity: sha512-6dbzkDxFSSCZr7b3Cm7AFvESRax8JpNYiIvI/ScUNPN+M7L2a3PKWKlfdcD/ZvWOAKmQW1l0j5GPH/ucHHu8sQ==, tarball: file:projects/love-resources.tgz}
id: file:projects/love-resources.tgz
name: '@rush-temp/love-resources'
version: 0.0.0
@@ -27543,7 +27594,7 @@ packages:
dev: false
file:projects/love.tgz(@types/node@20.11.19)(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2):
resolution: {integrity: sha512-IA1DOwfYE00ACQ47zgXq3yOcNghk1oU1nPcq15bqdjOxJeKaE5sa4w/yJ7WoGn1Dm5F0TmxKc67GGDxcTT6kDQ==, tarball: file:projects/love.tgz}
resolution: {integrity: sha512-D9QBf+/PREVaj/zqju1EnET+45ImzmEpqYThK+I2fbGxZ2t128DbfspBi3Lf2pUure8yF2PSTlTJSzKmUtgvpA==, tarball: file:projects/love.tgz}
id: file:projects/love.tgz
name: '@rush-temp/love'
version: 0.0.0
@@ -29340,7 +29391,7 @@ packages:
dev: false
file:projects/openai.tgz(esbuild@0.20.1)(svelte@4.2.12)(ts-node@10.9.2)(zod@3.23.8):
resolution: {integrity: sha512-jWsGOjlco80/7CIZ2Ogs7lCWr23lHUDQIB2hkPgYu2rbvM7iwSz5jhPjTPmR2y9gkVTSyyd9ozTo9LuxPpI6Kg==, tarball: file:projects/openai.tgz}
resolution: {integrity: sha512-ZFMlTpj1sADO+BtsNC9SGLFtId5nF8RpYNJhDyz1Q/Mt+c/pBk/ayUKOioOCjc4XTP+82rjue+kojBfDeFQuLw==, tarball: file:projects/openai.tgz}
id: file:projects/openai.tgz
name: '@rush-temp/openai'
version: 0.0.0
@@ -29355,6 +29406,7 @@ packages:
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
js-tiktoken: 1.0.14
openai: 4.56.0(zod@3.23.8)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.12)
@@ -29525,7 +29577,7 @@ packages:
dev: false
file:projects/pod-ai-bot.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)(zod@3.23.8):
resolution: {integrity: sha512-aMlSGJ89IhINflYpenenAXdbK+Vw6eAHu3R9rO1nsd7pmsOWxRLG6Hp5f+sN1q6S1cmPVQeD/q/tuvp6byFK8g==, tarball: file:projects/pod-ai-bot.tgz}
resolution: {integrity: sha512-hQkngAuFcwYMmIcVKDLMD0UnbZf3JuuXVVoWGG1w/7quPWksjcce8LX6hNrShCzN/+5LkpZVzNz8PAEHdX1M8A==, tarball: file:projects/pod-ai-bot.tgz}
id: file:projects/pod-ai-bot.tgz
name: '@rush-temp/pod-ai-bot'
version: 0.0.0
@@ -29552,10 +29604,12 @@ packages:
fast-equals: 5.0.1
form-data: 4.0.0
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
js-tiktoken: 1.0.14
mongodb: 6.9.0
node-fetch: 2.7.0
openai: 4.56.0(zod@3.23.8)
prettier: 3.2.5
puppeteer: 22.14.0(bufferutil@4.0.8)(typescript@5.3.3)(utf-8-validate@6.0.4)
tiktoken: 1.0.16
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.3.3)
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.3.3)
@@ -29813,7 +29867,7 @@ packages:
dev: false
file:projects/pod-github.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4)(y-prosemirror@1.2.12):
resolution: {integrity: sha512-WPkKYn8Uz2hTqWrzT7vjrfyFyOZctsvFVeixkSDH2RPydyXaB6tE2qIm0UFa5VgCnuBu9Rsc7nN62m2Ac7mL1w==, tarball: file:projects/pod-github.tgz}
resolution: {integrity: sha512-eyMBtVIlmdGMh+T89SuihlOHLsLA6czYiSY5SiZ06YL+VOb/TrBiluqvbWyMWUThZ1gFQCpUH/jm+0nkDUZg+Q==, tarball: file:projects/pod-github.tgz}
id: file:projects/pod-github.tgz
name: '@rush-temp/pod-github'
version: 0.0.0
@@ -30610,7 +30664,7 @@ packages:
dev: false
file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.1)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-cvUyK9P5nMldS6XkLY4agfy2Ch2aFf7Qv7rXU/goSeywHNOwEf/jgffVgsXFU6Z0TFqnWfiB/ey7SJ9pM/mU4Q==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-TnkAF2lFRAD6kQruc0x6uW65fsADXRJyTxBFx2mKJSsaZP57rWfmFDaO00am5SvSzGtUr2GguQvcCh+IeoPYrQ==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz
name: '@rush-temp/prod'
version: 0.0.0
+2
View File
@@ -201,6 +201,8 @@
"@hcengineering/analytics-collector": "^0.6.0",
"@hcengineering/analytics-collector-assets": "^0.6.0",
"@hcengineering/analytics-collector-resources": "^0.6.0",
"@hcengineering/ai-bot": "^0.6.0",
"@hcengineering/ai-bot-resources": "^0.6.0",
"electron-squirrel-startup": "~1.0.0",
"dotenv": "~16.0.0",
"electron-context-menu": "^4.0.1",
+3
View File
@@ -52,6 +52,7 @@ import { productsId } from '@hcengineering/products'
import { questionsId } from '@hcengineering/questions'
import { trainingId } from '@hcengineering/training'
import { documentsId } from '@hcengineering/controlled-documents'
import aiBot, { aiBotId } from '@hcengineering/ai-bot'
import '@hcengineering/activity-assets'
import '@hcengineering/attachment-assets'
@@ -234,6 +235,7 @@ export async function configurePlatform (): Promise<void> {
setMetadata(sign.metadata.SignURL, config.SIGN_URL)
setMetadata(uiPlugin.metadata.DefaultApplication, login.component.LoginApp)
setMetadata(analyticsCollector.metadata.EndpointURL, config.ANALYTICS_COLLECTOR_URL)
setMetadata(aiBot.metadata.EndpointURL, config.AI_URL)
const languages = myBranding.languages !== undefined && myBranding.languages !== '' ? myBranding.languages.split(',').map((l) => l.trim()) : ['en', 'ru', 'es', 'pt']
@@ -275,6 +277,7 @@ export async function configurePlatform (): Promise<void> {
addLocation(tagsId, async () => await import('@hcengineering/tags-resources'))
addLocation(calendarId, async () => await import('@hcengineering/calendar-resources'))
addLocation(analyticsCollectorId, async () => await import('@hcengineering/analytics-collector-resources'))
addLocation(aiBotId, async () => await import('@hcengineering/ai-bot-resources'))
addLocation(trackerId, async () => await import('@hcengineering/tracker-resources'))
addLocation(boardId, async () => await import('@hcengineering/board-resources'))
+1
View File
@@ -27,6 +27,7 @@ export interface Config {
PRINT_URL?: string
PUSH_PUBLIC_KEY: string
ANALYTICS_COLLECTOR_URL?: string
AI_URL?:string
BRANDING_URL?: string
PREVIEW_CONFIG: string
DESKTOP_UPDATES_URL?: string
+2
View File
@@ -276,6 +276,8 @@ services:
memory: 300M
aiBot:
image: hardcoreeng/ai-bot
ports:
- 4010:4010
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
+1
View File
@@ -10,3 +10,4 @@ PRINT_URL=http://localhost:4005
SIGN_URL=http://localhost:4006
ANALYTICS_COLLECTOR_URL=http://localhost:4007
AI_URL=http://localhost:4010
+2 -1
View File
@@ -5,5 +5,6 @@
"REKONI_URL": "http://localhost:4004",
"PRINT_URL": "http://localhost:4005",
"SIGN_URL": "http://localhost:4006",
"ANALYTICS_COLLECTOR_URL":"http://localhost:4007"
"ANALYTICS_COLLECTOR_URL":"http://localhost:4007",
"AI_URL": "http://localhost:4010"
}
+2
View File
@@ -231,6 +231,8 @@
"@hcengineering/products": "^0.1.0",
"@hcengineering/products-assets": "^0.1.0",
"@hcengineering/products-resources": "^0.1.0",
"@hcengineering/ai-bot": "^0.6.0",
"@hcengineering/ai-bot-resources": "^0.6.0",
"@sentry/svelte": "~7.101.0",
"posthog-js": "~1.122.0"
}
+1
View File
@@ -13,6 +13,7 @@
"PRINT_URL": "http://localhost:4005",
"SIGN_URL": "http://localhost:4006",
"ANALYTICS_COLLECTOR_URL": "http://localhost:4007",
"AI_URL": "http://localhost:4010",
"BRANDING_URL": "/branding.json",
"VERSION": null,
"MODEL_VERSION": null
+4
View File
@@ -59,6 +59,7 @@ import { documentsId } from '@hcengineering/controlled-documents'
import textEditor, { textEditorId } from '@hcengineering/text-editor'
import analyticsCollector, {analyticsCollectorId} from '@hcengineering/analytics-collector'
import { uploaderId } from '@hcengineering/uploader'
import aiBot, { aiBotId } from '@hcengineering/ai-bot'
import { bitrixId } from '@hcengineering/bitrix'
@@ -144,6 +145,7 @@ export interface Config {
ANALYTICS_COLLECTOR_URL?:string
BRANDING_URL?: string
TELEGRAM_BOT_URL?: string
AI_URL?:string
// Could be defined for dev environment
FRONT_URL?: string
@@ -307,6 +309,7 @@ export async function configurePlatform() {
setMetadata(calendar.metadata.CalendarServiceURL, config.CALENDAR_URL ?? 'http://localhost:8095')
setMetadata(notification.metadata.PushPublicKey, config.PUSH_PUBLIC_KEY)
setMetadata(analyticsCollector.metadata.EndpointURL, config.ANALYTICS_COLLECTOR_URL)
setMetadata(aiBot.metadata.EndpointURL, config.AI_URL)
setMetadata(github.metadata.GithubApplication, config.GITHUB_APP ?? '')
setMetadata(github.metadata.GithubClientID, config.GITHUB_CLIENTID ?? '')
@@ -365,6 +368,7 @@ export async function configurePlatform() {
addLocation(timeId, () => import(/* webpackChunkName: "time" */ '@hcengineering/time-resources'))
addLocation(desktopPreferencesId, () => import(/* webpackChunkName: "desktop-preferences" */ '@hcengineering/desktop-preferences-resources'))
addLocation(analyticsCollectorId, async () => await import('@hcengineering/analytics-collector-resources'))
addLocation(aiBotId, async () => await import('@hcengineering/ai-bot-resources'))
addLocation(trackerId, () => import(/* webpackChunkName: "tracker" */ '@hcengineering/tracker-resources'))
addLocation(boardId, () => import(/* webpackChunkName: "board" */ '@hcengineering/board-resources'))
+60 -16
View File
@@ -43,6 +43,11 @@ export function defineActions (builder: Builder): void {
chunter.category.Chunter
)
defineMessageActions(builder)
defineChannelActions(builder)
}
function defineMessageActions (builder: Builder): void {
createAction(
builder,
{
@@ -84,6 +89,61 @@ export function defineActions (builder: Builder): void {
chunter.action.CopyChatMessageLink
)
createAction(
builder,
{
action: chunter.actionImpl.DeleteChatMessage,
label: view.string.Delete,
icon: view.icon.Delete,
input: 'focus',
keyBinding: ['Backspace'],
category: chunter.category.Chunter,
target: chunter.class.ChatMessage,
visibilityTester: chunter.function.CanDeleteMessage,
context: { mode: ['context', 'browser'], group: 'remove' }
},
chunter.action.DeleteChatMessage
)
createAction(
builder,
{
action: chunter.actionImpl.TranslateMessage,
label: chunter.string.Translate,
icon: view.icon.Translate,
input: 'focus',
category: chunter.category.Chunter,
target: chunter.class.ChatMessage,
visibilityTester: chunter.function.CanTranslateMessage,
inline: true,
context: {
mode: 'context',
group: 'edit'
}
},
chunter.action.TranslateMessage
)
createAction(
builder,
{
action: chunter.actionImpl.ShowOriginalMessage,
label: chunter.string.ShowOriginal,
icon: view.icon.Undo,
input: 'focus',
category: chunter.category.Chunter,
target: chunter.class.ChatMessage,
visibilityTester: chunter.function.CanTranslateMessage,
inline: true,
context: {
mode: 'context',
group: 'edit'
}
},
chunter.action.ShowOriginalMessage
)
}
function defineChannelActions (builder: Builder): void {
createAction(
builder,
{
@@ -154,22 +214,6 @@ export function defineActions (builder: Builder): void {
}
})
createAction(
builder,
{
action: chunter.actionImpl.DeleteChatMessage,
label: view.string.Delete,
icon: view.icon.Delete,
input: 'focus',
keyBinding: ['Backspace'],
category: chunter.category.Chunter,
target: chunter.class.ChatMessage,
visibilityTester: chunter.function.CanDeleteMessage,
context: { mode: ['context', 'browser'], group: 'remove' }
},
chunter.action.DeleteChatMessage
)
createAction(
builder,
{
+3 -1
View File
@@ -52,7 +52,9 @@ export default mergeIds(chunterId, chunter, {
DeleteChatMessage: '' as ViewAction,
ReplyToThread: '' as ViewAction,
OpenInSidebar: '' as ViewAction,
OpenInSidebarTab: '' as ViewAction
OpenInSidebarTab: '' as ViewAction,
TranslateMessage: '' as ViewAction,
ShowOriginalMessage: '' as ViewAction
},
category: {
Chunter: '' as Ref<ActionCategory>
+3 -1
View File
@@ -15,7 +15,7 @@
import { Analytics } from '@hcengineering/analytics'
import '@hcengineering/platform-rig/profiles/ui/svelte'
import { writable } from 'svelte/store'
import { derived, writable } from 'svelte/store'
export { default as Theme } from './Theme.svelte'
@@ -78,3 +78,5 @@ export function initThemeStore (): void {
)
)
}
export const languageStore = derived(themeStore, ($theme) => $theme.language)
+1 -1
View File
@@ -47,7 +47,7 @@ export type {
MouseTargetEvent
} from './types'
export { themeStore } from '@hcengineering/theme'
export { themeStore, languageStore } from '@hcengineering/theme'
// export { applicationShortcutKey } from './utils'
export { getCurrentLocation, locationToUrl, navigate, location, setLocationStorageKey } from './location'
@@ -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>,
+1
View File
@@ -39,5 +39,6 @@ startFront(metricsContext, {
DESKTOP_UPDATES_URL: process.env.DESKTOP_UPDATES_URL,
DESKTOP_UPDATES_CHANNEL: process.env.DESKTOP_UPDATES_CHANNEL,
ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL,
AI_URL: process.env.AI_URL,
TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_URL
})
+5
View File
@@ -2081,6 +2081,11 @@
"projectFolder": "plugins/ai-bot",
"shouldPublish": false
},
{
"packageName": "@hcengineering/ai-bot-resources",
"projectFolder": "plugins/ai-bot-resources",
"shouldPublish": false
},
{
"packageName": "@hcengineering/model-server-ai-bot",
"projectFolder": "models/server-ai-bot",
+3 -2
View File
@@ -1,9 +1,10 @@
FROM node:20-alpine
FROM node:20
WORKDIR /usr/src/app
RUN npm install --ignore-scripts=false --verbose bufferutil utf-8-validate @mongodb-js/zstd snappy --unsafe-perm
COPY bundle/bundle.js ./
COPY assets/avatar.png ./
EXPOSE 4008
EXPOSE 4010
CMD [ "node", "bundle.js" ]
+1 -1
View File
@@ -82,7 +82,7 @@
"mongodb": "^6.9.0",
"node-fetch": "^2.6.6",
"openai": "^4.56.0",
"tiktoken": "^1.0.16",
"js-tiktoken": "^1.0.14",
"ws": "^8.18.0"
}
}
+21 -38
View File
@@ -30,54 +30,37 @@ interface Config {
Password: string
OpenAIKey: string
OpenAIModel: OpenAI.ChatModel
OpenAITranslateModel: OpenAI.ChatModel
MaxContentTokens: number
MaxHistoryRecords: number
}
const envMap: { [key in keyof Config]: string } = {
AccountsURL: 'ACCOUNTS_URL',
ConfigurationDB: 'CONFIGURATION_DB',
MongoURL: 'MONGO_URL',
ServerSecret: 'SERVER_SECRET',
ServiceID: 'SERVICE_ID',
SupportWorkspace: 'SUPPORT_WORKSPACE',
FirstName: 'FIRST_NAME',
LastName: 'LAST_NAME',
AvatarPath: 'AVATAR_PATH',
AvatarName: 'AVATAR_NAME',
AvatarContentType: 'AVATAR_CONTENT_TYPE',
Password: 'PASSWORD',
OpenAIKey: 'OPENAI_API_KEY',
OpenAIModel: 'OPENAI_MODEL',
MaxContentTokens: 'MAX_CONTENT_TOKENS',
MaxHistoryRecords: 'MAX_HISTORY_RECORDS'
Port: number
}
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
const config: Config = (() => {
const params: Partial<Config> = {
AccountsURL: process.env[envMap.AccountsURL],
ConfigurationDB: process.env[envMap.ConfigurationDB] ?? '%ai-bot',
MongoURL: process.env[envMap.MongoURL],
ServerSecret: process.env[envMap.ServerSecret],
ServiceID: process.env[envMap.ServiceID] ?? 'ai-bot-service',
SupportWorkspace: process.env[envMap.SupportWorkspace],
FirstName: process.env[envMap.FirstName],
LastName: process.env[envMap.LastName],
AvatarPath: process.env[envMap.AvatarPath] ?? './assets/avatar.png',
AvatarName: process.env[envMap.AvatarName] ?? 'huly_ai_bot_avatar',
AvatarContentType: process.env[envMap.AvatarContentType] ?? '.png',
Password: process.env[envMap.Password] ?? 'password',
OpenAIKey: process.env[envMap.OpenAIKey],
OpenAIModel: (process.env[envMap.OpenAIModel] ?? 'gpt-4o-mini') as OpenAI.ChatModel,
MaxContentTokens: parseNumber(process.env[envMap.MaxContentTokens]) ?? 128 * 100,
MaxHistoryRecords: parseNumber(process.env[envMap.MaxHistoryRecords]) ?? 500
AccountsURL: process.env.ACCOUNTS_URL,
ConfigurationDB: process.env.CONFIGURATION_DB ?? '%ai-bot',
MongoURL: process.env.MONGO_URL,
ServerSecret: process.env.SERVER_SECRET,
ServiceID: process.env.SERVICE_ID ?? 'ai-bot-service',
SupportWorkspace: process.env.SUPPORT_WORKSPACE,
FirstName: process.env.FIRST_NAME,
LastName: process.env.LAST_NAME,
AvatarPath: process.env.AVATAR_PATH ?? './assets/avatar.png',
AvatarName: process.env.AVATAR_NAME ?? 'huly_ai_bot_avatar',
AvatarContentType: process.env.AVATAR_CONTENT_TYPE ?? '.png',
Password: process.env.PASSWORD ?? 'password',
OpenAIKey: process.env.OPENAI_API_KEY ?? '',
OpenAIModel: (process.env.OPENAI_MODEL ?? 'gpt-4o-mini') as OpenAI.ChatModel,
OpenAITranslateModel: (process.env.OPENAI_TRANSLATE_MODEL ?? 'gpt-4o-mini') as OpenAI.ChatModel,
MaxContentTokens: parseNumber(process.env.MAX_CONTENT_TOKENS) ?? 128 * 100,
MaxHistoryRecords: parseNumber(process.env.MAX_HISTORY_RECORDS) ?? 500,
Port: parseNumber(process.env.PORT) ?? 4010
}
const missingEnv = (Object.keys(params) as Array<keyof Config>)
.filter((key) => params[key] === undefined)
.map((key) => envMap[key])
const missingEnv = (Object.keys(params) as Array<keyof Config>).filter((key) => params[key] === undefined)
if (missingEnv.length > 0) {
throw Error(`Missing env variables: ${missingEnv.join(', ')}`)
+35 -7
View File
@@ -14,13 +14,14 @@
//
import { MeasureContext, systemAccountEmail, isWorkspaceCreating } from '@hcengineering/core'
import { aiBotAccountEmail, AIBotTransferEvent } from '@hcengineering/ai-bot'
import { aiBotAccountEmail, AIBotTransferEvent, TranslateResponse, TranslateRequest } from '@hcengineering/ai-bot'
import { WorkspaceInfoRecord } from '@hcengineering/server-ai-bot'
import { getTransactorEndpoint } from '@hcengineering/server-client'
import { generateToken } from '@hcengineering/server-token'
import { WorkspaceLoginInfo } from '@hcengineering/account'
import OpenAI from 'openai'
import { encoding_for_model } from 'tiktoken'
import { encodingForModel } from 'js-tiktoken'
import { htmlToMarkup, markupToHTML } from '@hcengineering/text'
import { WorkspaceClient } from './workspaceClient'
import { assignBotToWorkspace, getWorkspaceInfo } from './account'
@@ -39,8 +40,8 @@ export class AIBotController {
private readonly intervalId: NodeJS.Timeout
readonly aiClient: OpenAI
readonly encoding = encoding_for_model(config.OpenAIModel)
readonly aiClient?: OpenAI
readonly encoding = encodingForModel(config.OpenAIModel)
assignTimeout: NodeJS.Timeout | undefined
assignAttempts = 0
@@ -49,7 +50,7 @@ export class AIBotController {
readonly storage: DbStorage,
private readonly ctx: MeasureContext
) {
this.aiClient = new OpenAI({ apiKey: config.OpenAIKey })
this.aiClient = config.OpenAIKey === '' ? undefined : new OpenAI({ apiKey: config.OpenAIKey })
this.intervalId = setInterval(() => {
void this.updateWorkspaceClients()
@@ -199,8 +200,6 @@ export class AIBotController {
async close (): Promise<void> {
clearInterval(this.intervalId)
this.encoding.free()
for (const workspace of this.workspaces.values()) {
await workspace.close()
}
@@ -213,6 +212,35 @@ export class AIBotController {
async updateAvatarInfo (workspace: string, path: string, lastModified: number): Promise<void> {
await this.storage.updateWorkspace(workspace, { $set: { avatarPath: path, avatarLastModified: lastModified } })
}
async translate (req: TranslateRequest): Promise<TranslateResponse | undefined> {
if (this.aiClient === undefined) {
return undefined
}
const html = markupToHTML(req.text)
const start = Date.now()
const response = await this.aiClient.chat.completions.create({
model: config.OpenAITranslateModel,
messages: [
{
role: 'system',
content: `Your task is to translate the text into ${req.lang} while preserving the html structure and metadata`
},
{
role: 'user',
content: html
}
]
})
const end = Date.now()
this.ctx.info('Translation time: ', { time: end - start })
const result = response.choices[0].message.content
const text = result !== null ? htmlToMarkup(result) : req.text
return {
text,
lang: req.lang
}
}
}
async function wait (delay: number): Promise<void> {
+39
View File
@@ -0,0 +1,39 @@
//
// 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.
//
const generateMessage = (code: number): string => {
if (code === 401) {
return 'Unauthorized'
}
if (code === 404) {
return 'Not Found'
}
if (code === 400) {
return 'Bad Request'
}
return 'Error'
}
export class ApiError extends Error {
readonly code: number
constructor (code: number, message?: string) {
super(message ?? generateMessage(code))
this.code = code
}
}
+131
View File
@@ -0,0 +1,131 @@
//
// 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 { Token, decodeToken } from '@hcengineering/server-token'
import cors from 'cors'
import express, { type Express, type NextFunction, type Request, type Response } from 'express'
import { IncomingHttpHeaders, type Server } from 'http'
import { TranslateRequest } from '@hcengineering/ai-bot'
import { ApiError } from './error'
import { AIBotController } from './controller'
const extractCookieToken = (cookie?: string): Token | null => {
if (cookie === undefined || cookie === null) {
return null
}
const cookies = cookie.split(';')
const tokenCookie = cookies.find((cookie) => cookie.toLocaleLowerCase().includes('token'))
if (tokenCookie === undefined) {
return null
}
const encodedToken = tokenCookie.split('=')[1]
if (encodedToken === undefined) {
return null
}
return decodeToken(encodedToken)
}
const extractAuthorizationToken = (authorization?: string): Token | null => {
if (authorization === undefined || authorization === null) {
return null
}
const encodedToken = authorization.split(' ')[1]
if (encodedToken === undefined) {
return null
}
return decodeToken(encodedToken)
}
const extractToken = (headers: IncomingHttpHeaders): Token => {
try {
const token = extractCookieToken(headers.cookie) ?? extractAuthorizationToken(headers.authorization)
if (token === null) {
throw new ApiError(401)
}
return token
} catch {
throw new ApiError(401)
}
}
type AsyncRequestHandler = (req: Request, res: Response, token: Token, next: NextFunction) => Promise<void>
const handleRequest = async (
fn: AsyncRequestHandler,
req: Request,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const token = extractToken(req.headers)
await fn(req, res, token, next)
} catch (err: unknown) {
next(err)
}
}
const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, next: NextFunction) => {
void handleRequest(fn, req, res, next)
}
export function createServer (controller: AIBotController): Express {
const app = express()
app.use(cors())
app.use(express.json())
app.post(
'/translate',
wrapRequest(async (req, res) => {
if (req.body == null || Array.isArray(req.body) || typeof req.body !== 'object') {
throw new ApiError(400)
}
const response = await controller.translate(req.body as TranslateRequest)
if (response === undefined) {
throw new ApiError(500)
}
res.status(200)
res.json(response)
})
)
app.use((err: any, _req: any, res: any, _next: any) => {
console.log(err)
if (err instanceof ApiError) {
res.status(err.code).send({ code: err.code, message: err.message })
return
}
res.status(500).send(err.message?.length > 0 ? { message: err.message } : err)
})
return app
}
export function listen (e: Express, port: number, host?: string): Server {
const cb = (): void => {
console.log(`AI service has been started at ${host ?? '*'}:${port}`)
}
return host !== undefined ? e.listen(port, host, cb) : e.listen(port, cb)
}
+4
View File
@@ -24,6 +24,7 @@ import { closeDB, DbStorage, getDB } from './storage'
import { AIBotController } from './controller'
import { createBotAccount } from './account'
import { registerLoaders } from './loaders'
import { createServer, listen } from './server'
export const start = async (): Promise<void> => {
setMetadata(serverToken.metadata.Secret, config.ServerSecret)
@@ -49,10 +50,13 @@ export const start = async (): Promise<void> => {
await new Promise((resolve) => setTimeout(resolve, 3000))
}
const aiController = new AIBotController(storage, ctx)
const app = createServer(aiController)
const server = listen(app, config.Port)
const onClose = (): void => {
void aiController.close()
void closeDB()
server.close(() => process.exit())
}
process.on('SIGINT', onClose)
+1 -1
View File
@@ -22,7 +22,7 @@ import { deepEqual } from 'fast-equals'
import notification from '@hcengineering/notification'
import OpenAI from 'openai'
import { countTokens } from '@hcengineering/openai'
import { Tiktoken } from 'tiktoken'
import { Tiktoken } from 'js-tiktoken'
import { HistoryRecord } from './types'
import config from './config'
@@ -366,6 +366,7 @@ export class WorkspaceClient {
objectId: Ref<Doc>,
objectClass: Ref<Class<Doc>>
): Promise<void> {
if (this.controller.aiClient === undefined) return
if (this.summarizing.has(objectId)) {
return
}
@@ -422,6 +423,7 @@ export class WorkspaceClient {
}
async processResponseEvent (event: AIBotResponseEvent): Promise<void> {
if (this.controller.aiClient === undefined) return
const client = await this.opClient
const hierarchy = client.getHierarchy()