diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index c9621aca010..d5c534c2be2 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -77,6 +77,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-assets': + specifier: file:./projects/ai-bot-assets.tgz + version: file:projects/ai-bot-assets.tgz(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) @@ -25637,8 +25640,38 @@ packages: - ts-node dev: false + file:projects/ai-bot-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2): + resolution: {integrity: sha512-xwtMv8lV+TmOJZ5SEA0YWNgptx+v9YRXtsR8uawwDJDOBkwsmpRoEsP3g/bXGijptA1T7VpNgPyreAOlGSqZZw==, tarball: file:projects/ai-bot-assets.tgz} + id: file:projects/ai-bot-assets.tgz + name: '@rush-temp/ai-bot-assets' + version: 0.0.0 + dependencies: + '@types/jest': 29.5.12 + '@types/node': 20.11.19 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2) + 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.6.2) + 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) + jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2) + prettier: 3.2.5 + ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.6.2) + typescript: 5.6.2 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - 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-VUzWp9Le++agPkh/Sq5V8y1LHVSofd+WOJNwkYFDWlJH0+TKM1F88CFBANOYUlg2ZvhOQGyLCCFTcjbPohfnSQ==, tarball: file:projects/ai-bot-resources.tgz} + resolution: {integrity: sha512-rgc3ngDxitLmyUF5HCGSeZCeqweXu2+jmgj8vHK2CpPWjbBxLd8tF2lGYlicpU+9JLhnl0j0by5NciFKyWRvfA==, tarball: file:projects/ai-bot-resources.tgz} id: file:projects/ai-bot-resources.tgz name: '@rush-temp/ai-bot-resources' version: 0.0.0 @@ -26451,7 +26484,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-qC9oDROK1wvmwya82IbB9HAj4bYw3oBPa23nMIW8phhnkSp+6fCVboz3D8G1EIw5eZmDbPCSgtnZZh+mH/VqLw==, tarball: file:projects/chunter-resources.tgz} + resolution: {integrity: sha512-XNRPFippUn7IcdWBIN89K2yw5BTyGIWVtWXFmshvu/Z3JwAU5LRh0AgkGQIkgc4wEPlfnjQahECRwCW0YM5Rpw==, tarball: file:projects/chunter-resources.tgz} id: file:projects/chunter-resources.tgz name: '@rush-temp/chunter-resources' version: 0.0.0 @@ -27174,7 +27207,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-bAR3xXsPX0SRBzBNKHnhr4KdUZibYwt2pOQBKp1pcybJXnw9pwv8A7S6wYWgaG6o091zASbDD/EGvxOqZ/bdSQ==, tarball: file:projects/desktop.tgz} + resolution: {integrity: sha512-zb6Uxzob+Vga320WvAnXKvB9O6c5hrpStA/z/HA9eVLKMruUiQ1OwSmfnHWlcDEgfYPyOvdiBNHf0NAdYzZ6tQ==, tarball: file:projects/desktop.tgz} id: file:projects/desktop.tgz name: '@rush-temp/desktop' version: 0.0.0 @@ -28863,7 +28896,7 @@ packages: dev: false file:projects/model-ai-bot.tgz: - resolution: {integrity: sha512-tdvrfRa0PZZCWrUq5QqiZi4Bex97OgR7tMHVLMUJhhg7lJOnR9nyjFKn+X0lEI79pi0sllds53Ksdmi5Kd+OlQ==, tarball: file:projects/model-ai-bot.tgz} + resolution: {integrity: sha512-11PeZ6Wr6JqkZn4ft4j7VSR696l5GsLHLD+1z4PmlZfKjE42cPXtoEVI6DaqNN8LJxvJ5nxWX+T/w56XKejIlw==, tarball: file:projects/model-ai-bot.tgz} name: '@rush-temp/model-ai-bot' version: 0.0.0 dependencies: @@ -30112,7 +30145,7 @@ packages: dev: false file:projects/model-text-editor.tgz: - resolution: {integrity: sha512-z1TlVHF8VxcMeChSZ70ZGJsKdGSKisnpnPGuLmp2yYS30ckD5ohoj4KeqgbKu+qT+72FYSg4COXFoAYnszgfoA==, tarball: file:projects/model-text-editor.tgz} + resolution: {integrity: sha512-9s3E5iyN8yKzKTCaufGL6nwxOtSCbD2izkcbS3c346ej/K7yyUzW1v5bWDrSuDIE12P+i+i4BbGagwK4WffypQ==, tarball: file:projects/model-text-editor.tgz} name: '@rush-temp/model-text-editor' version: 0.0.0 dependencies: @@ -30149,7 +30182,7 @@ packages: dev: false file:projects/model-tracker.tgz: - resolution: {integrity: sha512-u9m53Xf1czr2mPQRN62kUs6umFh0ThtAahFquvZSdqY3bDjd1rYpG31SUOHphnGBTa4um183/pxGdJLtD599YQ==, tarball: file:projects/model-tracker.tgz} + resolution: {integrity: sha512-eaLvC0aM9jMISJOHLbmMC5yQKKHWyrkL+WTn4YhuwnJzZagCjRPFH4dWR4lr1JaDNIndI7h4arZILZWApwz3UA==, tarball: file:projects/model-tracker.tgz} name: '@rush-temp/model-tracker' version: 0.0.0 dependencies: @@ -31804,7 +31837,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-TnkAF2lFRAD6kQruc0x6uW65fsADXRJyTxBFx2mKJSsaZP57rWfmFDaO00am5SvSzGtUr2GguQvcCh+IeoPYrQ==, tarball: file:projects/prod.tgz} + resolution: {integrity: sha512-kl1uUD52HqmPYcwNmNX8/fuZFF3scp1JPXFbfS+B6K42WKPLXDONHGfTsHuwjgenIRKxp2sJVWYv7hB7I9pakw==, tarball: file:projects/prod.tgz} id: file:projects/prod.tgz name: '@rush-temp/prod' version: 0.0.0 @@ -33934,7 +33967,7 @@ packages: dev: false file:projects/server-pipeline.tgz: - resolution: {integrity: sha512-gaF/29tMBmt1rcHqrdJ87g7xHkb39eb8oZfDEVzyeK7qwf1vRjOYE0el4Pp/5OvkBrZSQGEwKgfUK3lHI3iLJQ==, tarball: file:projects/server-pipeline.tgz} + resolution: {integrity: sha512-DlpNsigfMkh58YF59z09JalNTXUwN1Aci6Rlwvt5dYu+wMx8TDtQpPqGtgWEdnaCPo9cbB/R23Ey4UUF1iI4qA==, tarball: file:projects/server-pipeline.tgz} name: '@rush-temp/server-pipeline' version: 0.0.0 dependencies: diff --git a/desktop/package.json b/desktop/package.json index 9250b66912e..2570de86c50 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -203,6 +203,7 @@ "@hcengineering/analytics-collector-assets": "^0.6.0", "@hcengineering/analytics-collector-resources": "^0.6.0", "@hcengineering/ai-bot": "^0.6.0", + "@hcengineering/ai-bot-assets": "^0.6.0", "@hcengineering/ai-bot-resources": "^0.6.0", "electron-squirrel-startup": "~1.0.0", "dotenv": "~16.0.0", diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index 359874b35f6..02060905e8c 100644 --- a/desktop/src/ui/platform.ts +++ b/desktop/src/ui/platform.ts @@ -55,6 +55,7 @@ import { documentsId } from '@hcengineering/controlled-documents' import aiBot, { aiBotId } from '@hcengineering/ai-bot' import '@hcengineering/activity-assets' +import '@hcengineering/ai-bot-assets' import '@hcengineering/attachment-assets' import '@hcengineering/bitrix-assets' import '@hcengineering/board-assets' @@ -185,6 +186,7 @@ function configureI18n (): void { addStringsLoader(loveId, async (lang: string) => await import(`@hcengineering/love-assets/lang/${lang}.json`)) addStringsLoader(printId, async (lang: string) => await import(`@hcengineering/print-assets/lang/${lang}.json`)) addStringsLoader(analyticsCollectorId, async (lang: string) => await import(`@hcengineering/analytics-collector-assets/lang/${lang}.json`)) + addStringsLoader(aiBotId, async (lang: string) => await import(`@hcengineering/ai-bot-assets/lang/${lang}.json`)) } export async function configurePlatform (): Promise { diff --git a/dev/prod/package.json b/dev/prod/package.json index 652bed4f338..98a1712bff0 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -232,6 +232,7 @@ "@hcengineering/products-assets": "^0.1.0", "@hcengineering/products-resources": "^0.1.0", "@hcengineering/ai-bot": "^0.6.0", + "@hcengineering/ai-bot-assets": "^0.6.0", "@hcengineering/ai-bot-resources": "^0.6.0", "@sentry/svelte": "~7.101.0", "posthog-js": "~1.122.0" diff --git a/dev/prod/src/platform.ts b/dev/prod/src/platform.ts index a43cf316fce..9e38649353e 100644 --- a/dev/prod/src/platform.ts +++ b/dev/prod/src/platform.ts @@ -64,6 +64,7 @@ import aiBot, { aiBotId } from '@hcengineering/ai-bot' import { bitrixId } from '@hcengineering/bitrix' import '@hcengineering/activity-assets' +import '@hcengineering/ai-bot-assets' import '@hcengineering/attachment-assets' import '@hcengineering/bitrix-assets' import '@hcengineering/board-assets' @@ -224,6 +225,7 @@ function configureI18n(): void { addStringsLoader(loveId, async (lang: string) => await import(`@hcengineering/love-assets/lang/${lang}.json`)) addStringsLoader(printId, async (lang: string) => await import(`@hcengineering/print-assets/lang/${lang}.json`)) addStringsLoader(analyticsCollectorId, async (lang: string) => await import(`@hcengineering/analytics-collector-assets/lang/${lang}.json`)) + addStringsLoader(aiBotId, async (lang: string) => await import(`@hcengineering/ai-bot-assets/lang/${lang}.json`)) } export async function configurePlatform() { diff --git a/models/ai-bot/package.json b/models/ai-bot/package.json index fe96dbca4c1..7e0224bc44d 100644 --- a/models/ai-bot/package.json +++ b/models/ai-bot/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "@hcengineering/ai-bot": "^0.6.0", + "@hcengineering/text-editor": "^0.6.0", "@hcengineering/analytics-collector": "^0.6.0", "@hcengineering/chunter": "^0.6.20", "@hcengineering/core": "^0.6.32", diff --git a/models/ai-bot/src/index.ts b/models/ai-bot/src/index.ts index 7393934da58..a8542b62913 100644 --- a/models/ai-bot/src/index.ts +++ b/models/ai-bot/src/index.ts @@ -17,6 +17,8 @@ import { type Builder } from '@hcengineering/model' import core, { type Domain } from '@hcengineering/core' import chunter from '@hcengineering/chunter' import analyticsCollector from '@hcengineering/analytics-collector' +import textEditor from '@hcengineering/text-editor' +import view from '@hcengineering/view' import aiBot from './plugin' @@ -31,4 +33,16 @@ export function createModel (builder: Builder): void { ofClass: analyticsCollector.class.OnboardingChannel, component: aiBot.component.OnboardingChannelPanelExtension }) + + builder.createDoc(textEditor.class.TextEditorInlineCommand, core.space.Model, { + command: 'translate', + commandTemplate: '/translate [lang] [text]', + title: aiBot.string.Translate, + description: aiBot.string.InstantlyTranslateText, + icon: view.icon.Translate, + category: 'general', + type: 'command', + action: aiBot.command.Translate, + visibilityTester: aiBot.function.IsAiEnabled + }) } diff --git a/models/ai-bot/src/plugin.ts b/models/ai-bot/src/plugin.ts index 0a0977b16e0..0a49b04919a 100644 --- a/models/ai-bot/src/plugin.ts +++ b/models/ai-bot/src/plugin.ts @@ -13,12 +13,19 @@ // limitations under the License. // -import { mergeIds } from '@hcengineering/platform' +import { type Resource, mergeIds } from '@hcengineering/platform' import aiBot, { aiBotId } from '@hcengineering/ai-bot' import type { AnyComponent } from '@hcengineering/ui/src/types' +import { type InlineCommandAction, type InlineCommandVisibilityTester } from '@hcengineering/text-editor' export default mergeIds(aiBotId, aiBot, { + function: { + IsAiEnabled: '' as Resource + }, component: { OnboardingChannelPanelExtension: '' as AnyComponent + }, + command: { + Translate: '' as Resource } }) diff --git a/models/chunter/src/index.ts b/models/chunter/src/index.ts index b8bb7da2ca0..0ffb359dd09 100644 --- a/models/chunter/src/index.ts +++ b/models/chunter/src/index.ts @@ -38,7 +38,9 @@ import { TObjectChatPanel, TThreadMessage, TTypingInfo, - TChunterExtension + TChunterExtension, + TPrivateThreadMessage, + TPrivateChatMessage } from './types' export { chunterId } from '@hcengineering/chunter' @@ -57,7 +59,9 @@ export function createModel (builder: Builder): void { TChatSyncInfo, TInlineButton, TTypingInfo, - TChunterExtension + TChunterExtension, + TPrivateThreadMessage, + TPrivateChatMessage ) builder.createDoc( diff --git a/models/chunter/src/types.ts b/models/chunter/src/types.ts index 73c7213eecc..79812a2d088 100644 --- a/models/chunter/src/types.ts +++ b/models/chunter/src/types.ts @@ -38,7 +38,9 @@ import type { ObjectChatPanel, ThreadMessage, TypingInfo, - ChunterExtensionPoint + ChunterExtensionPoint, + PrivateThreadMessage, + PrivateChatMessage } from '@hcengineering/chunter' import { type Class, @@ -50,7 +52,11 @@ import { type Ref, type Timestamp } from '@hcengineering/core' -import contact, { type ChannelProvider as SocialChannelProvider, type Person } from '@hcengineering/contact' +import contact, { + type ChannelProvider as SocialChannelProvider, + type Person, + type PersonSpace +} from '@hcengineering/contact' import activity, { type ActivityMessage } from '@hcengineering/activity' import { TActivityMessage } from '@hcengineering/model-activity' import attachment from '@hcengineering/model-attachment' @@ -119,6 +125,18 @@ export class TThreadMessage extends TChatMessage implements ThreadMessage { objectClass!: Ref> } +@Model(chunter.class.PrivateChatMessage, chunter.class.ChatMessage) +@UX(chunter.string.Message, chunter.icon.Thread, undefined, undefined, undefined, chunter.string.Threads) +export class TPrivateChatMessage extends TChatMessage implements PrivateChatMessage { + declare space: Ref +} + +@Model(chunter.class.PrivateThreadMessage, chunter.class.ThreadMessage) +@UX(chunter.string.ThreadMessage, chunter.icon.Thread, undefined, undefined, undefined, chunter.string.Threads) +export class TPrivateThreadMessage extends TThreadMessage implements PrivateThreadMessage { + declare space: Ref +} + @Model(chunter.class.ChatMessageViewlet, core.class.Doc, DOMAIN_MODEL) export class TChatMessageViewlet extends TDoc implements ChatMessageViewlet { @Prop(TypeRef(core.class.Doc), core.string.Class) diff --git a/models/text-editor/package.json b/models/text-editor/package.json index 39bfbc9703f..ee52ffd0038 100644 --- a/models/text-editor/package.json +++ b/models/text-editor/package.json @@ -35,6 +35,7 @@ "@hcengineering/ui": "^0.6.15", "@hcengineering/text": "^0.6.5", "@hcengineering/text-editor": "^0.6.0", - "@hcengineering/model-core": "^0.6.0" + "@hcengineering/model-core": "^0.6.0", + "@hcengineering/view": "^0.6.13" } } diff --git a/models/text-editor/src/index.ts b/models/text-editor/src/index.ts index cbf19db02e1..b734a4dc0a0 100644 --- a/models/text-editor/src/index.ts +++ b/models/text-editor/src/index.ts @@ -28,8 +28,16 @@ import { type TextActionActiveFunction, type ActiveDescriptor, type TogglerDescriptor, - type TextEditorActionKind + type TextEditorActionKind, + type TextEditorInlineCommand, + type InlineShortcutAction, + type TextEditorInlineCommandCategory, + type TextEditorInlineCommandType, + type InlineCommandAction, + type RefInputActionDisabledFn, + type InlineCommandVisibilityTester } from '@hcengineering/text-editor' +import view from '@hcengineering/view' // eslint-disable-next-line @typescript-eslint/no-unused-vars import type { EditorKitOptions } from '@hcengineering/text-editor-resources' import textEditor from './plugin' @@ -43,9 +51,11 @@ export type { RefInputAction, RefInputActionItem } export class TRefInputActionItem extends TDoc implements RefInputActionItem { label!: IntlString icon!: Asset + iconProps?: Record // Query for documents with pattern action!: Resource + isDisabledFn?: Resource } @Model(textEditor.class.TextEditorExtensionFactory, core.class.Doc, DOMAIN_MODEL) @@ -66,6 +76,22 @@ export class TTextEditorAction extends TDoc implements TextEditorAction { index!: number } +@Model(textEditor.class.TextEditorInlineCommand, core.class.Doc, DOMAIN_MODEL) +export class TTextEditorInlineCommand extends TDoc implements TextEditorInlineCommand { + icon!: Asset + title!: IntlString + description?: IntlString + + command!: string + commandTemplate?: string + + category!: TextEditorInlineCommandCategory + type!: TextEditorInlineCommandType + + action!: Resource | Resource + visibilityTester?: Resource +} + function createHeaderAction (builder: Builder, level: number): void { let icon: Asset switch (level) { @@ -148,7 +174,7 @@ function createImageAlignmentAction (builder: Builder, align: 'center' | 'left' } export function createModel (builder: Builder): void { - builder.createModel(TRefInputActionItem, TTextEditorExtensionFactory, TTextEditorAction) + builder.createModel(TRefInputActionItem, TTextEditorExtensionFactory, TTextEditorAction, TTextEditorInlineCommand) createHeaderAction(builder, 1) createHeaderAction(builder, 2) @@ -361,4 +387,90 @@ export function createModel (builder: Builder): void { category: 110, index: 5 }) + + builder.createDoc( + textEditor.class.RefInputActionItem, + core.space.Model, + { + label: textEditor.string.ShortcutsAndCommands, + icon: view.icon.Slash, + iconProps: { + size: 'small' + }, + action: textEditor.action.ShowCommands, + order: 6000, + isDisabledFn: textEditor.function.DisableInlineCommands + }, + textEditor.ids.CommandsPopupAction + ) + + builder.createDoc( + textEditor.class.TextEditorInlineCommand, + core.space.Model, + { + command: 'image', + title: textEditor.string.Image, + icon: view.icon.Image, + category: 'editor', + type: 'shortcut', + action: textEditor.inlineCommandImpl.InsertImage + }, + textEditor.inlineCommand.InsertImage + ) + + builder.createDoc( + textEditor.class.TextEditorInlineCommand, + core.space.Model, + { + command: 'table', + title: textEditor.string.Table, + icon: view.icon.Table2, + category: 'editor', + type: 'shortcut', + action: textEditor.inlineCommandImpl.InsertTable + }, + textEditor.inlineCommand.InsertTable + ) + + builder.createDoc( + textEditor.class.TextEditorInlineCommand, + core.space.Model, + { + command: 'code-block', + title: textEditor.string.CodeBlock, + icon: view.icon.CodeBlock, + category: 'editor', + type: 'shortcut', + action: textEditor.inlineCommandImpl.InsertCodeBlock + }, + textEditor.inlineCommand.InsertCodeBlock + ) + + builder.createDoc( + textEditor.class.TextEditorInlineCommand, + core.space.Model, + { + command: 'separator-line', + title: textEditor.string.SeparatorLine, + icon: view.icon.SeparatorLine, + category: 'editor', + type: 'shortcut', + action: textEditor.inlineCommandImpl.InsertSeparatorLine + }, + textEditor.inlineCommand.InsertSeparatorLine + ) + + builder.createDoc( + textEditor.class.TextEditorInlineCommand, + core.space.Model, + { + command: 'todo-list', + title: textEditor.string.TodoList, + icon: view.icon.TodoList, + category: 'editor', + type: 'shortcut', + action: textEditor.inlineCommandImpl.InsertTodoList + }, + textEditor.inlineCommand.InsertTodoList + ) } diff --git a/models/text-editor/src/plugin.ts b/models/text-editor/src/plugin.ts index ad954bb19f2..8ce0f98e949 100644 --- a/models/text-editor/src/plugin.ts +++ b/models/text-editor/src/plugin.ts @@ -18,7 +18,8 @@ import { mergeIds, type Resource } from '@hcengineering/platform' import textEditor, { type TextActionFunction, type TextActionVisibleFunction, - textEditorId + textEditorId, + type RefInputActionDisabledFn } from '@hcengineering/text-editor' export default mergeIds(textEditorId, textEditor, { @@ -34,6 +35,7 @@ export default mergeIds(textEditorId, textEditor, { IsEditableTableActive: '' as Resource, IsEditableNote: '' as Resource, IsEditable: '' as Resource, - IsHeadingVisible: '' as Resource + IsHeadingVisible: '' as Resource, + DisableInlineCommands: '' as Resource } }) diff --git a/models/tracker/package.json b/models/tracker/package.json index 042cb489043..e5d2e7be842 100644 --- a/models/tracker/package.json +++ b/models/tracker/package.json @@ -53,6 +53,7 @@ "@hcengineering/ui": "^0.6.15", "@hcengineering/view": "^0.6.13", "@hcengineering/workbench": "^0.6.16", - "@hcengineering/model-preference": "^0.6.0" + "@hcengineering/model-preference": "^0.6.0", + "@hcengineering/text-editor": "^0.6.0" } } diff --git a/models/tracker/src/actions.ts b/models/tracker/src/actions.ts index e42af938fa4..8e180ec64a7 100644 --- a/models/tracker/src/actions.ts +++ b/models/tracker/src/actions.ts @@ -22,6 +22,7 @@ import workbench, { createNavigateAction } from '@hcengineering/model-workbench' import { type IntlString } from '@hcengineering/platform' import { TrackerEvents, trackerId } from '@hcengineering/tracker' import { type KeyBinding } from '@hcengineering/view' +import textEditor from '@hcengineering/text-editor' import tracker from './plugin' import tags from '@hcengineering/tags' @@ -204,6 +205,16 @@ export function createActions (builder: Builder, issuesId: string, componentsId: tracker.action.NewIssue ) + builder.createDoc(textEditor.class.TextEditorInlineCommand, core.space.Model, { + command: 'createIssue', + title: tracker.string.NewIssue, + icon: tracker.icon.NewIssue, + description: tracker.string.CreateIssueDescription, + category: 'general', + type: 'shortcut', + action: tracker.command.CreateIssue + }) + createAction( builder, { diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index e7449c1113b..0320d584873 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -25,6 +25,7 @@ import tracker from '@hcengineering/tracker-resources/src/plugin' import type { AnyComponent } from '@hcengineering/ui/src/types' import { type Action, type ViewAction, type Viewlet } from '@hcengineering/view' import { type Application } from '@hcengineering/workbench' +import { type InlineShortcutAction } from '@hcengineering/text-editor' export default mergeIds(trackerId, tracker, { string: { @@ -122,5 +123,8 @@ export default mergeIds(trackerId, tracker, { function: { SetComponentStore: '' as Resource<(manager: DocManager) => void>, ComponentFilterFunction: '' as Resource<(doc: Doc, target: Doc) => boolean> + }, + command: { + CreateIssue: '' as Resource } }) diff --git a/packages/presentation/src/components/markup/NodeContent.svelte b/packages/presentation/src/components/markup/NodeContent.svelte index 80eb2449865..61478560e1d 100644 --- a/packages/presentation/src/components/markup/NodeContent.svelte +++ b/packages/presentation/src/components/markup/NodeContent.svelte @@ -109,6 +109,15 @@ {/each} {/if} + {:else if node.type === MarkupNodeType.inlineCommand} + {@const command = toString(attrs.command)} + {#if command !== undefined} + /{command} + {:else if nodes.length > 0} + {#each nodes as node} + + {/each} + {/if} {:else if node.type === MarkupNodeType.hard_break}
{:else if node.type === MarkupNodeType.ordered_list} diff --git a/packages/text/src/kits/server-kit.ts b/packages/text/src/kits/server-kit.ts index 1b01d876f6e..b1fcb4f0e89 100644 --- a/packages/text/src/kits/server-kit.ts +++ b/packages/text/src/kits/server-kit.ts @@ -26,6 +26,7 @@ import { NodeUuid } from '../marks/nodeUuid' import { FileNode, FileOptions } from '../nodes/file' import { ImageNode, ImageOptions } from '../nodes/image' import { ReferenceNode } from '../nodes/reference' +import { InlineCommandNode } from '../nodes/inlineCommand' import { TodoItemNode, TodoListNode } from '../nodes/todo' import { CodeBlockExtension, codeBlockOptions } from '../nodes' @@ -90,6 +91,7 @@ export const ServerKit = Extension.create({ TodoListNode, ReferenceNode, CommentNode, + InlineCommandNode, NodeUuid, NoteBaseExtension ] diff --git a/packages/text/src/markup/model.ts b/packages/text/src/markup/model.ts index 453f729ea4b..7573389a9e4 100644 --- a/packages/text/src/markup/model.ts +++ b/packages/text/src/markup/model.ts @@ -25,6 +25,7 @@ export enum MarkupNodeType { image = 'image', file = 'file', reference = 'reference', + inlineCommand = 'inlineCommand', hard_break = 'hardBreak', ordered_list = 'orderedList', bullet_list = 'bulletList', diff --git a/packages/text/src/markup/utils.ts b/packages/text/src/markup/utils.ts index 016a4c64ee9..296b4f74b5b 100644 --- a/packages/text/src/markup/utils.ts +++ b/packages/text/src/markup/utils.ts @@ -21,7 +21,7 @@ import { Node as ProseMirrorNode, Schema } from '@tiptap/pm/model' import { deepEqual } from 'fast-equals' import { defaultExtensions } from '../extensions' import { nodeDoc, nodeParagraph, nodeText } from './dsl' -import { MarkupMark, MarkupNode, MarkupNodeType, emptyMarkupNode } from './model' +import { emptyMarkupNode, MarkupMark, MarkupNode, MarkupNodeType } from './model' /** @public */ export const EmptyMarkup: Markup = jsonToMarkup(emptyMarkupNode()) @@ -40,6 +40,28 @@ export function isEmptyMarkup (markup: Markup | undefined): boolean { return isEmptyNode(markupToJSON(markup)) } +export function getInlineCommand (markup: Markup | undefined): string | undefined { + if (markup === undefined || markup === null || markup === '') { + return + } + + const node = markupToJSON(markup) + + if (node.type !== MarkupNodeType.doc) { + return + } + + const content = node.content ?? [] + const firstParagraph = content[0] + if (firstParagraph === undefined || firstParagraph.type !== MarkupNodeType.paragraph) { + return + } + + if (firstParagraph.content?.[0].type === MarkupNodeType.inlineCommand) { + return firstParagraph.content?.[0]?.attrs?.command as string + } +} + /** @public */ export function areEqualMarkups (markup1: Markup, markup2: Markup): boolean { if (markup1 === markup2) { @@ -100,6 +122,7 @@ const nonEmptyNodes = [ MarkupNodeType.horizontal_rule, MarkupNodeType.image, MarkupNodeType.reference, + MarkupNodeType.inlineCommand, MarkupNodeType.subLink, MarkupNodeType.table ] @@ -159,6 +182,7 @@ export function markupToJSON (markup: Markup): MarkupNode { /** @public */ export function jsonToPmNode (json: MarkupNode, schema?: Schema, extensions?: Extensions): ProseMirrorNode { schema ??= extensions == null ? defaultSchema : getSchema(extensions ?? defaultExtensions) + return ProseMirrorNode.fromJSON(schema, json) } @@ -263,13 +287,15 @@ export function stripTags (markup: Markup, textLimit = 0, extensions: Extensions textParts.push(WHITESPACE) charCount++ } - } else if (node.type.name === 'reference') { + } else if (node.type.name === MarkupNodeType.reference) { const label = node.attrs.label ?? '' pushText(label.length > 0 ? `@${label}` : '') + } else if (node.type.name === MarkupNodeType.inlineCommand) { + const command = node.attrs.command ?? '' + pushText(`/${command}`) } return true }) - const result = textParts.join('') - return result + return textParts.join('') } diff --git a/packages/text/src/nodes/inlineCommand.ts b/packages/text/src/nodes/inlineCommand.ts new file mode 100644 index 00000000000..efa0b37aca7 --- /dev/null +++ b/packages/text/src/nodes/inlineCommand.ts @@ -0,0 +1,70 @@ +// +// 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 { mergeAttributes, Node } from '@tiptap/core' + +import { getDataAttribute } from './utils' + +export interface InlineCommandOptions { + renderLabel: (props: { options: InlineCommandOptions, node: any }) => string + suggestion: { char: string } +} + +export const InlineCommandNode = Node.create({ + name: 'inlineCommand', + group: 'inline', + inline: true, + + addAttributes () { + return { + command: getDataAttribute('command') + } + }, + + addOptions () { + return { + renderLabel ({ options, node }) { + return `${options.suggestion.char}${node.attrs.command ?? ''}` + }, + suggestion: { char: '/' } + } + }, + + parseHTML () { + return [ + { + tag: `span[data-type="${this.name}"]` + } + ] + }, + + renderHTML ({ node, HTMLAttributes }) { + const options = this.options + return [ + 'span', + mergeAttributes( + { + 'data-type': this.name + }, + HTMLAttributes + ), + this.options.renderLabel({ options, node }) + ] + }, + + renderText ({ node }) { + const options = this.options + return options.renderLabel({ options, node }) + } +}) diff --git a/packages/theme/styles/common.scss b/packages/theme/styles/common.scss index 87c6982bc22..80f94f6aa57 100644 --- a/packages/theme/styles/common.scss +++ b/packages/theme/styles/common.scss @@ -479,6 +479,11 @@ cursor: pointer; } +.inlineCommand { + display: inline-flex; + width: fit-content; +} + .antiDivider { margin: .25rem 0; min-height: 1px; diff --git a/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte b/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte index faeaf70f5ba..d556cc94c99 100644 --- a/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte +++ b/plugins/activity-resources/src/components/activity-message/ActivityMessageTemplate.svelte @@ -174,88 +174,90 @@ on:click={onClick} on:contextmenu={handleContextMenu} > - {#if showNotify && !embedded && !isShort} -
- {/if} - {#if embedded} -
- {:else if isShort} - - - - {:else} -
- {#if $$slots.icon} - - {:else if person} - - {:else} - - {/if} - {#if isSaved} -
- -
- {/if} - {#if socialIcon} - - {/if} -
- {/if} -
- {#if !isShort} -
- {#if person} -
- -
+ +
+ {#if showNotify && !embedded && !isShort} +
+ {/if} + {#if embedded} +
+ {:else if isShort} + + + + {:else} +
+ {#if $$slots.icon} + + {:else if person} + {:else} -
-
- {/if} - - {#if !skipLabel} - + {/if} - - {#if !skipLabel && showDatePreposition} - - - {/if} - - - - - {#if message.editedOn} - ( + {#if isSaved} +
+ +
{/if} - - {#if withActions && inlineActions.length > 0 && !readonly} -
- {#each inlineActions as item} - - {/each} + {#if socialIcon} + {/if}
{/if} +
+ {#if !isShort} +
+ {#if person} +
+ +
+ {:else} +
+
+ {/if} + + {#if !skipLabel} + + {/if} + + {#if !skipLabel && showDatePreposition} + + + {/if} - + + + + {#if message.editedOn} + ( + {/if} + + {#if withActions && inlineActions.length > 0 && !readonly} +
+ {#each inlineActions as item} + + {/each} +
+ {/if} +
+ {/if} - {#if !hideFooter} - - {/if} - - {#if parentMessage && showEmbedded} -
- - {/if} -
+ + {#if !hideFooter} + + {/if} + + {#if parentMessage && showEmbedded} +
+ + {/if} +
+
{#if withActions && !readonly}
.time { + &:hover > .inner > .time { visibility: visible; } - .time { - display: flex; - justify-content: end; - width: 2.5rem; - min-width: 2.5rem; - visibility: hidden; - margin-top: 0.125rem; - } - &.actionsOpened { &.borderedHover { border: 1px solid var(--global-ui-BackgroundColor); diff --git a/plugins/ai-bot-assets/.eslintrc.js b/plugins/ai-bot-assets/.eslintrc.js new file mode 100644 index 00000000000..e73094bc7e5 --- /dev/null +++ b/plugins/ai-bot-assets/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/plugins/ai-bot-assets/config/rig.json b/plugins/ai-bot-assets/config/rig.json new file mode 100644 index 00000000000..b75800b9b70 --- /dev/null +++ b/plugins/ai-bot-assets/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "assets" +} diff --git a/plugins/ai-bot-assets/jest.config.js b/plugins/ai-bot-assets/jest.config.js new file mode 100644 index 00000000000..2cfd408b679 --- /dev/null +++ b/plugins/ai-bot-assets/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/plugins/ai-bot-assets/lang/en.json b/plugins/ai-bot-assets/lang/en.json new file mode 100644 index 00000000000..931f9ae1e16 --- /dev/null +++ b/plugins/ai-bot-assets/lang/en.json @@ -0,0 +1,10 @@ +{ + "string": { + "Translate": "Translate", + "InstantlyTranslateText": "Instantly translate text", + "TranslateHelp": "Please use the following format to translate text:", + "Where": "Where", + "CodesHelp": "is equal to one of the following language codes:", + "CantTranslateThisText": "Cant translate this text." + } +} \ No newline at end of file diff --git a/plugins/ai-bot-assets/lang/es.json b/plugins/ai-bot-assets/lang/es.json new file mode 100644 index 00000000000..dadd5fb4d91 --- /dev/null +++ b/plugins/ai-bot-assets/lang/es.json @@ -0,0 +1,10 @@ +{ + "string": { + "Translate": "Traducir", + "InstantlyTranslateText": "Traducir texto instantáneamente", + "TranslateHelp": "Por favor, use el siguiente formato para traducir texto:", + "Where": "Dónde", + "CodesHelp": "es igual a uno de los siguientes códigos de idioma:", + "CantTranslateThisText": "No se puede traducir este texto." + } +} \ No newline at end of file diff --git a/plugins/ai-bot-assets/lang/fr.json b/plugins/ai-bot-assets/lang/fr.json new file mode 100644 index 00000000000..ad3a0b8becc --- /dev/null +++ b/plugins/ai-bot-assets/lang/fr.json @@ -0,0 +1,10 @@ +{ + "string": { + "Translate": "Traduire", + "InstantlyTranslateText": "Traduire instantanément le texte", + "TranslateHelp": "Veuillez utiliser le format suivant pour traduire le texte:", + "Where": "Où", + "CodesHelp": "est égal à l'un des codes de langue suivants:", + "CantTranslateThisText": "Impossible de traduire ce texte." + } +} \ No newline at end of file diff --git a/plugins/ai-bot-assets/lang/pt.json b/plugins/ai-bot-assets/lang/pt.json new file mode 100644 index 00000000000..ad3a0b8becc --- /dev/null +++ b/plugins/ai-bot-assets/lang/pt.json @@ -0,0 +1,10 @@ +{ + "string": { + "Translate": "Traduire", + "InstantlyTranslateText": "Traduire instantanément le texte", + "TranslateHelp": "Veuillez utiliser le format suivant pour traduire le texte:", + "Where": "Où", + "CodesHelp": "est égal à l'un des codes de langue suivants:", + "CantTranslateThisText": "Impossible de traduire ce texte." + } +} \ No newline at end of file diff --git a/plugins/ai-bot-assets/lang/ru.json b/plugins/ai-bot-assets/lang/ru.json new file mode 100644 index 00000000000..3cccc1fba30 --- /dev/null +++ b/plugins/ai-bot-assets/lang/ru.json @@ -0,0 +1,10 @@ +{ + "string": { + "Translate": "Перевести", + "InstantlyTranslateText": "Мгновенно перевести текст", + "TranslateHelp": "Пожалуйста, используйте следующий формат для перевода текста:", + "Where": "Где", + "CodesHelp": "равно одному из следующих кодов языка:", + "CantTranslateThisText": "Невозможно перевести этот текст." + } +} \ No newline at end of file diff --git a/plugins/ai-bot-assets/lang/zh.json b/plugins/ai-bot-assets/lang/zh.json new file mode 100644 index 00000000000..6112f99e188 --- /dev/null +++ b/plugins/ai-bot-assets/lang/zh.json @@ -0,0 +1,10 @@ +{ + "string": { + "Translate": "翻译", + "InstantlyTranslateText": "即时翻译文本", + "TranslateHelp": "请使用以下格式翻译文本:", + "Where": "哪里", + "CodesHelp": "等于以下语言代码之一:", + "CantTranslateThisText": "无法翻译此文本。" + } +} diff --git a/plugins/ai-bot-assets/package.json b/plugins/ai-bot-assets/package.json new file mode 100644 index 00000000000..5df86be64cb --- /dev/null +++ b/plugins/ai-bot-assets/package.json @@ -0,0 +1,43 @@ +{ + "name": "@hcengineering/ai-bot-assets", + "version": "0.6.0", + "main": "src/index.ts", + "author": "Anticrm Platform Contributors", + "template": "@hcengineering/assets-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "test": "jest --passWithNoTests --silent", + "format": "format src", + "build:watch": "compile", + "build:docs": "", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.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": "^8.54.0", + "prettier": "^3.1.0", + "typescript": "^5.3.3", + "@types/node": "~20.11.16", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5" + }, + "dependencies": { + "@hcengineering/platform": "^0.6.11", + "@hcengineering/ai-bot": "^0.6.0" + }, + "repository": "https://github.com/hcenginneing/anticrm", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + } +} diff --git a/plugins/ai-bot-assets/src/index.ts b/plugins/ai-bot-assets/src/index.ts new file mode 100644 index 00000000000..f65ece33363 --- /dev/null +++ b/plugins/ai-bot-assets/src/index.ts @@ -0,0 +1,14 @@ +// +// 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. +// diff --git a/plugins/ai-bot-assets/tsconfig.json b/plugins/ai-bot-assets/tsconfig.json new file mode 100644 index 00000000000..4449c73536a --- /dev/null +++ b/plugins/ai-bot-assets/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "types": ["node", "jest"], + "tsBuildInfoFile": ".build/build.tsbuildinfo" + } +} \ No newline at end of file diff --git a/plugins/ai-bot-resources/package.json b/plugins/ai-bot-resources/package.json index d186d873ab8..878f7b9a52d 100644 --- a/plugins/ai-bot-resources/package.json +++ b/plugins/ai-bot-resources/package.json @@ -39,12 +39,18 @@ }, "dependencies": { "@hcengineering/ai-bot": "^0.6.0", + "@hcengineering/activity": "^0.6.0", "@hcengineering/analytics-collector": "^0.6.0", + "@hcengineering/chunter": "^0.6.20", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/contact-resources": "^0.6.0", "@hcengineering/core": "^0.6.32", "@hcengineering/platform": "^0.6.11", "@hcengineering/presentation": "^0.6.3", + "@hcengineering/text": "^0.6.5", + "@hcengineering/text-editor": "^0.6.0", + "@hcengineering/text-editor-resources": "^0.6.0", "@hcengineering/ui": "^0.6.15", - "@hcengineering/chunter": "^0.6.20", "svelte": "^4.2.12" } } diff --git a/plugins/ai-bot-resources/src/index.ts b/plugins/ai-bot-resources/src/index.ts index 6c179a5a7be..f1bebe2bbec 100644 --- a/plugins/ai-bot-resources/src/index.ts +++ b/plugins/ai-bot-resources/src/index.ts @@ -14,12 +14,20 @@ // import { type Resources } from '@hcengineering/platform' + import OnboardingChannelPanelExtension from './components/OnboardingChannelAsideExtension.svelte' +import { translateInlineCommand, isAiEnabled } from './utils' export * from './utils' export default async (): Promise => ({ + function: { + IsAiEnabled: isAiEnabled + }, component: { OnboardingChannelPanelExtension + }, + command: { + Translate: translateInlineCommand } }) diff --git a/plugins/ai-bot-resources/src/langs.ts b/plugins/ai-bot-resources/src/langs.ts new file mode 100644 index 00000000000..1887432bdd0 --- /dev/null +++ b/plugins/ai-bot-resources/src/langs.ts @@ -0,0 +1,103 @@ +export const languages = { + af: 'Afrikaans', + sq: 'Albanian', + am: 'Amharic', + ar: 'Arabic', + hy: 'Armenian', + az: 'Azerbaijani', + eu: 'Basque', + be: 'Belarusian', + bn: 'Bengali', + bs: 'Bosnian', + bg: 'Bulgarian', + ca: 'Catalan', + ceb: 'Cebuano', + ny: 'Chichewa', + zh: 'Chinese', + co: 'Corsican', + hr: 'Croatian', + cs: 'Czech', + da: 'Danish', + nl: 'Dutch', + en: 'English', + eo: 'Esperanto', + et: 'Estonian', + tl: 'Filipino', + fi: 'Finnish', + fr: 'French', + fy: 'Frisian', + gl: 'Galician', + ka: 'Georgian', + de: 'German', + el: 'Greek', + gu: 'Gujarati', + ht: 'Haitian Creole', + ha: 'Hausa', + haw: 'Hawaiian', + iw: 'Hebrew', + hi: 'Hindi', + hmn: 'Hmong', + hu: 'Hungarian', + is: 'Icelandic', + ig: 'Igbo', + id: 'Indonesian', + ga: 'Irish', + it: 'Italian', + ja: 'Japanese', + jw: 'Javanese', + kn: 'Kannada', + kk: 'Kazakh', + km: 'Khmer', + ko: 'Korean', + ky: 'Kyrgyz', + lo: 'Lao', + la: 'Latin', + lv: 'Latvian', + lt: 'Lithuanian', + lb: 'Luxembourgish', + mk: 'Macedonian', + mg: 'Malagasy', + ms: 'Malay', + ml: 'Malayalam', + mt: 'Maltese', + mi: 'Maori', + mr: 'Marathi', + mn: 'Mongolian', + ne: 'Nepali', + no: 'Norwegian', + ps: 'Pashto', + fa: 'Persian', + pl: 'Polish', + pt: 'Portuguese', + pa: 'Punjabi', + ro: 'Romanian', + ru: 'Russian', + sm: 'Samoan', + gd: 'Scots Gaelic', + sr: 'Serbian', + st: 'Sesotho', + sn: 'Shona', + sd: 'Sindhi', + si: 'Sinhala', + sk: 'Slovak', + sl: 'Slovenian', + so: 'Somali', + es: 'Spanish', + su: 'Sundanese', + sw: 'Swahili', + sv: 'Swedish', + tg: 'Tajik', + ta: 'Tamil', + te: 'Telugu', + th: 'Thai', + tr: 'Turkish', + uk: 'Ukrainian', + ur: 'Urdu', + uz: 'Uzbek', + vi: 'Vietnamese', + cy: 'Welsh', + xh: 'Xhosa', + yi: 'Yiddish', + yo: 'Yoruba', + zu: 'Zulu' +} diff --git a/plugins/ai-bot-resources/src/utils.ts b/plugins/ai-bot-resources/src/utils.ts index 718050bd86a..525ec8b3b45 100644 --- a/plugins/ai-bot-resources/src/utils.ts +++ b/plugins/ai-bot-resources/src/utils.ts @@ -12,12 +12,29 @@ // 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 { concatLink, type Doc, type Markup } from '@hcengineering/core' +import { getMetadata, translate as translateS } from '@hcengineering/platform' +import presentation, { getClient } from '@hcengineering/presentation' import { type TranslateRequest, type TranslateResponse } from '@hcengineering/ai-bot' +import { type InlineCommandAction } from '@hcengineering/text-editor' +import { parseInlineCommands } from '@hcengineering/text-editor-resources' +import { createPrivateMessage, createPrivateThreadMessage } from '@hcengineering/chunter' +import { personSpaceStore } from '@hcengineering/contact-resources' +import { + htmlToMarkup, + jsonToMarkup, + MarkupMarkType, + type MarkupNode, + MarkupNodeType, + markupToJSON, + nodeDoc +} from '@hcengineering/text' +import { languageStore } from '@hcengineering/ui' +import { get } from 'svelte/store' +import activity, { type ActivityMessage } from '@hcengineering/activity' import aiBot from './plugin' +import { languages } from './langs' export async function translate (text: Markup, lang: string): Promise { const url = getMetadata(aiBot.metadata.EndpointURL) ?? '' @@ -47,3 +64,131 @@ export async function translate (text: Markup, lang: string): Promise { + const lang = get(languageStore) + return jsonToMarkup({ + type: MarkupNodeType.doc, + content: [ + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: await translateS(aiBot.string.TranslateHelp, {}, lang) + } + ] + }, + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + marks: [{ type: MarkupMarkType.code, attrs: {} }], + text: '/translate [lang] [text to translate]' + } + ] + }, + { + type: MarkupNodeType.paragraph, + content: [ + { + type: MarkupNodeType.text, + text: await translateS(aiBot.string.Where, {}, lang) + }, + { + type: MarkupNodeType.text, + marks: [{ type: MarkupMarkType.code, attrs: {} }], + text: ' [lang] ' + }, + { + type: MarkupNodeType.text, + text: await translateS(aiBot.string.CodesHelp, {}, lang) + } + ] + }, + { + type: MarkupNodeType.paragraph, + content: getLanguageMarkupNodes() + } + ] + }) +} + +function getLanguageMarkupNodes (): MarkupNode[] { + return Object.entries(languages) + .map(([key, value]) => [ + { + type: MarkupNodeType.text, + marks: [{ type: MarkupMarkType.code, attrs: {} }], + text: key + }, + { + type: MarkupNodeType.text, + text: ` (${value})` + }, + { + type: MarkupNodeType.hard_break, + content: [] + } + ]) + .flatMap((x) => x) +} + +async function sendMessage (markup: Markup, doc: Pick): Promise { + const client = getClient() + const hierarchy = client.getHierarchy() + + if (hierarchy.isDerived(doc._class, activity.class.ActivityMessage)) { + const msg = await client.findOne(doc._class, { _id: doc._id }) + if (msg === undefined) return + await createPrivateThreadMessage( + client, + markup, + get(personSpaceStore)._id, + msg as ActivityMessage, + aiBot.account.AIBot + ) + } else { + await createPrivateMessage(client, markup, get(personSpaceStore)._id, doc, aiBot.account.AIBot) + } +} + +export const translateInlineCommand: InlineCommandAction = async (markup, context) => { + const { + args: [lang], + markup: clearMarkup + } = parseInlineCommands(markup, 1) + + if ((languages as any)[lang] === undefined) { + await sendMessage(await getHelpMarkup(), { _id: context.objectId, _class: context.objectClass }) + return + } + + const resp = await translate(clearMarkup, lang) + + if (resp === undefined) { + const message = await translateS(aiBot.string.CantTranslateThisText, {}, get(languageStore)) + await sendMessage(htmlToMarkup(message), { _id: context.objectId, _class: context.objectClass }) + return + } + + const result = nodeDoc( + { + type: MarkupNodeType.blockquote, + content: markupToJSON(clearMarkup).content + }, + { + type: MarkupNodeType.paragraph, + content: markupToJSON(resp.text).content + } + ) + + await sendMessage(jsonToMarkup(result), { _id: context.objectId, _class: context.objectClass }) +} diff --git a/plugins/ai-bot/src/index.ts b/plugins/ai-bot/src/index.ts index e7748b01297..e96367fbfb1 100644 --- a/plugins/ai-bot/src/index.ts +++ b/plugins/ai-bot/src/index.ts @@ -14,7 +14,7 @@ // import { Account, Class, Doc, type Mixin, Ref, Space } from '@hcengineering/core' -import type { Metadata, Plugin } from '@hcengineering/platform' +import type { IntlString, Metadata, Plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { ChatMessage } from '@hcengineering/chunter' @@ -67,6 +67,14 @@ const aiBot = plugin(aiBotId, { }, account: { AIBot: '' as Ref + }, + string: { + Translate: '' as IntlString, + InstantlyTranslateText: '' as IntlString, + TranslateHelp: '' as IntlString, + Where: '' as IntlString, + CodesHelp: '' as IntlString, + CantTranslateThisText: '' as IntlString } }) diff --git a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte index c8fb1f79236..8e10d869ed0 100644 --- a/plugins/attachment-resources/src/components/AttachmentRefInput.svelte +++ b/plugins/attachment-resources/src/components/AttachmentRefInput.svelte @@ -33,6 +33,7 @@ import attachment from '../plugin' import AttachmentPresenter from './AttachmentPresenter.svelte' + export let context: { objectId: Ref, objectClass: Ref> } | undefined = undefined export let objectId: Ref export let space: Ref export let _class: Ref> @@ -340,6 +341,7 @@ > { this.updatesDates(res) this.metadataStore.set(res) @@ -276,11 +281,12 @@ export class ChannelDataProvider implements IChannelDataProvider { this.tailStart = start } + const personSpace = get(personSpaceStore) this.tailQuery.query( this.msgClass, { attachedTo: this.chatId, - space: this.space, + space: personSpace !== undefined ? { $in: [this.space, personSpace._id] } : this.space, ...query, ...(this.tailStart !== undefined ? { createdOn: { $gte: this.tailStart } } : {}) }, @@ -330,11 +336,12 @@ export class ChannelDataProvider implements IChannelDataProvider { const client = getClient() const skipIds = this.getChunkSkipIds(loadAfter) + const personSpace = get(personSpaceStore) let messages: ActivityMessage[] = await client.findAll( this.msgClass, { attachedTo: this.chatId, - space: this.space, + space: personSpace !== undefined ? { $in: [this.space, personSpace._id] } : this.space, createdOn: equal ? isBackward ? { $lte: loadAfter } diff --git a/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte b/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte index 934d1baa10d..44ac3ad7c1c 100644 --- a/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte +++ b/plugins/chunter-resources/src/components/chat-message/ChatMessageInput.svelte @@ -245,6 +245,7 @@ {focusIndex} bind:this={inputRef} bind:content={inputContent} + context={{ objectId: object._id, objectClass: object._class }} {_class} space={getChannelSpace(object._class, object._id, object.space)} skipAttachmentsPreload={(currentMessage.attachments ?? 0) === 0} diff --git a/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte b/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte index 3ac694bd8a2..9b6184ceb66 100644 --- a/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte +++ b/plugins/chunter-resources/src/components/chat-message/ChatMessagePresenter.svelte @@ -19,7 +19,7 @@ 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 { Action, Button, Icon, IconEdit, Label, ShowMore } from '@hcengineering/ui' import view from '@hcengineering/view' import activity, { ActivityMessage, ActivityMessageViewType, DisplayActivityMessage } from '@hcengineering/activity' import { ActivityDocLink, ActivityMessageTemplate, MessageInlineAction } from '@hcengineering/activity-resources' @@ -215,6 +215,11 @@ } else { displayText = value?.message ?? EmptyMarkup } + + $: isPrivate = + value && + (hierarchy.isDerived(value._class, chunter.class.PrivateChatMessage) || + hierarchy.isDerived(value._class, chunter.class.PrivateThreadMessage)) {#if inline && object} @@ -246,7 +251,7 @@ {skipLabel} {pending} {stale} - {readonly} + readonly={readonly || isPrivate} excludedActions={$shownTranslatedMessagesStore.has(value._id) ? [chunter.action.TranslateMessage] : [chunter.action.ShowOriginalMessage]} @@ -256,6 +261,16 @@ {type} {onClick} > + + {#if isPrivate && !isShort} +
+
+ +
+
+ {/if} +
@@ -307,3 +322,13 @@ {/if} + + diff --git a/plugins/chunter/src/index.ts b/plugins/chunter/src/index.ts index 6cf62e1afa7..a13051d848e 100644 --- a/plugins/chunter/src/index.ts +++ b/plugins/chunter/src/index.ts @@ -20,7 +20,7 @@ import type { Asset, Plugin, Resource } from '@hcengineering/platform' import { IntlString, plugin } from '@hcengineering/platform' import { AnyComponent } from '@hcengineering/ui' import { Action } from '@hcengineering/view' -import { Person, ChannelProvider as SocialChannelProvider } from '@hcengineering/contact' +import { Person, PersonSpace, ChannelProvider as SocialChannelProvider } from '@hcengineering/contact' import { Widget, WidgetTab } from '@hcengineering/workbench' /** @@ -71,6 +71,14 @@ export interface ThreadMessage extends ChatMessage { objectClass: Ref> } +export interface PrivateChatMessage extends ChatMessage { + space: Ref +} + +export interface PrivateThreadMessage extends ThreadMessage { + space: Ref +} + /** * @public */ @@ -163,7 +171,9 @@ export default plugin(chunterId, { ChatSyncInfo: '' as Ref>, InlineButton: '' as Ref>, TypingInfo: '' as Ref>, - ChunterExtension: '' as Ref> + ChunterExtension: '' as Ref>, + PrivateChatMessage: '' as Ref>, + PrivateThreadMessage: '' as Ref> }, mixin: { ObjectChatPanel: '' as Ref> @@ -228,7 +238,8 @@ export default plugin(chunterId, { StartConversation: '' as IntlString, ViewingThreadFromArchivedChannel: '' as IntlString, ViewingArchivedChannel: '' as IntlString, - OpenChatInSidebar: '' as IntlString + OpenChatInSidebar: '' as IntlString, + OnlyVisibleToYou: '' as IntlString }, ids: { DMNotification: '' as Ref, diff --git a/plugins/chunter/src/utils.ts b/plugins/chunter/src/utils.ts index 362e20b42f5..1777ee335c5 100644 --- a/plugins/chunter/src/utils.ts +++ b/plugins/chunter/src/utils.ts @@ -1,8 +1,23 @@ +// +// 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 { deepEqual } from 'fast-equals' -import core, { Ref, TxOperations } from '@hcengineering/core' -import { PersonAccount } from '@hcengineering/contact' +import core, { Account, Doc, Markup, Ref, TxOperations } from '@hcengineering/core' +import { PersonAccount, PersonSpace } from '@hcengineering/contact' +import { ActivityMessage } from '@hcengineering/activity' -import chunter, { DirectMessage } from '.' +import chunter, { DirectMessage, PrivateChatMessage, PrivateThreadMessage } from '.' /** * @public @@ -28,3 +43,48 @@ export async function getDirectChannel ( members: accIds }) } + +export async function createPrivateMessage ( + client: TxOperations, + markup: Markup, + space: Ref, + context: Pick, + account: Ref +): Promise { + await client.addCollection( + chunter.class.PrivateChatMessage, + space, + context._id, + context._class, + 'messages', + { message: markup, attachments: 0 }, + undefined, + undefined, + account + ) +} + +export async function createPrivateThreadMessage ( + client: TxOperations, + markup: Markup, + space: Ref, + context: Pick, + account: Ref +): Promise { + await client.addCollection( + chunter.class.PrivateThreadMessage, + space, + context._id, + context._class, + 'replies', + { + message: markup, + attachments: 0, + objectClass: context.attachedToClass, + objectId: context.attachedTo + }, + undefined, + undefined, + account + ) +} diff --git a/plugins/contact-resources/src/utils.ts b/plugins/contact-resources/src/utils.ts index 93700daccce..00cc36ae594 100644 --- a/plugins/contact-resources/src/utils.ts +++ b/plugins/contact-resources/src/utils.ts @@ -26,7 +26,8 @@ import { getLastName, getName, type Person, - type PersonAccount + type PersonAccount, + type PersonSpace } from '@hcengineering/contact' import core, { type Account, @@ -316,6 +317,7 @@ export const personAccountByPersonId = derived(personAccountByIdStore, (vals) => }) export const statusByUserStore = writable, UserStatus>>(new Map()) +export const personSpaceStore = writable() export const personByIdStore = derived([personAccountPersonByIdStore, employeeByIdStore], (vals) => { const m1 = Array.from(vals[0].entries()) @@ -328,7 +330,11 @@ function fillStores (): void { if (client !== undefined) { const accountPersonQuery = createQuery(true) - + const personSpaceQuery = createQuery(true) + const me = getCurrentAccount() as PersonAccount + personSpaceQuery.query(contact.class.PersonSpace, { person: me.person }, (res) => { + personSpaceStore.set(res[0]) + }) const query = createQuery(true) query.query(contact.mixin.Employee, { active: { $in: [true, false] } }, (res) => { employeesStore.set(res) diff --git a/plugins/text-editor-assets/lang/en.json b/plugins/text-editor-assets/lang/en.json index cbc870e0824..8de1d3ed724 100644 --- a/plugins/text-editor-assets/lang/en.json +++ b/plugins/text-editor-assets/lang/en.json @@ -59,6 +59,7 @@ "Unset": "Unset", "Image": "Image", "SeparatorLine": "Separator line", - "TodoList": "Action item" + "TodoList": "Action item", + "ShortcutsAndCommands": "Shortcuts & commands" } } diff --git a/plugins/text-editor-assets/lang/es.json b/plugins/text-editor-assets/lang/es.json index d10e9c46cb7..f2c96f58885 100644 --- a/plugins/text-editor-assets/lang/es.json +++ b/plugins/text-editor-assets/lang/es.json @@ -57,6 +57,7 @@ "Unset": "Anular", "Image": "Imagen", "SeparatorLine": "Línea de separación", - "TodoList": "Tarea pendiente" + "TodoList": "Tarea pendiente", + "ShortcutsAndCommands": "Atajos y comandos" } } \ No newline at end of file diff --git a/plugins/text-editor-assets/lang/fr.json b/plugins/text-editor-assets/lang/fr.json index 7193686f05c..8294a6ac495 100644 --- a/plugins/text-editor-assets/lang/fr.json +++ b/plugins/text-editor-assets/lang/fr.json @@ -57,6 +57,7 @@ "Unset": "Non défini", "Image": "Image", "SeparatorLine": "Ligne de séparation", - "TodoList": "À faire" + "TodoList": "À faire", + "ShortcutsAndCommands": "Raccourcis et commandes" } } \ No newline at end of file diff --git a/plugins/text-editor-assets/lang/pt.json b/plugins/text-editor-assets/lang/pt.json index b6f0b471258..9365c614b33 100644 --- a/plugins/text-editor-assets/lang/pt.json +++ b/plugins/text-editor-assets/lang/pt.json @@ -57,6 +57,7 @@ "Unset": "Anular", "Image": "Imagem", "SeparatorLine": "linha separadora", - "TodoList": "Tarefa pendente" + "TodoList": "Tarefa pendente", + "ShortcutsAndCommands": "Atalhos e comandos" } } diff --git a/plugins/text-editor-assets/lang/ru.json b/plugins/text-editor-assets/lang/ru.json index f197e3abe55..a62a498e7c9 100644 --- a/plugins/text-editor-assets/lang/ru.json +++ b/plugins/text-editor-assets/lang/ru.json @@ -59,6 +59,7 @@ "Unset": "Убрать", "Image": "Изображение", "SeparatorLine": "Разделительная линия", - "TodoList": "Действие" + "TodoList": "Действие", + "ShortcutsAndCommands": "Горячие клавиши и команды" } } diff --git a/plugins/text-editor-assets/lang/zh.json b/plugins/text-editor-assets/lang/zh.json index c3def4da635..3a04e364066 100644 --- a/plugins/text-editor-assets/lang/zh.json +++ b/plugins/text-editor-assets/lang/zh.json @@ -59,6 +59,7 @@ "Unset": "未设置", "Image": "图片", "SeparatorLine": "分隔线", - "TodoList": "待办" + "TodoList": "待办", + "ShortcutsAndCommands": "快捷键和命令" } } diff --git a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte index 7f551e54e23..acf774f86e6 100644 --- a/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte +++ b/plugins/text-editor-resources/src/components/CollaborativeTextEditor.svelte @@ -17,8 +17,8 @@ + +
+
+ +
+
+
+ {#if value.type === 'command'} + {#if value.commandTemplate} + {value.commandTemplate} + {:else} + /{value.command} + {/if} + {:else} + {value.title} + {/if} +
+ {#if twoLines} +
+ {#if value.description} + {value.description} + {/if} +
+ {/if} +
+
+ + diff --git a/plugins/text-editor-resources/src/components/InlineCommandsList.svelte b/plugins/text-editor-resources/src/components/InlineCommandsList.svelte index 57ecc2dbb17..094f99ea266 100644 --- a/plugins/text-editor-resources/src/components/InlineCommandsList.svelte +++ b/plugins/text-editor-resources/src/components/InlineCommandsList.svelte @@ -14,24 +14,24 @@ // limitations under the License. --> @@ -113,7 +115,7 @@ updateStyle() }} > - +
diff --git a/plugins/text-editor-resources/src/components/ReferenceInput.svelte b/plugins/text-editor-resources/src/components/ReferenceInput.svelte index 2851084e471..96bd82e9964 100644 --- a/plugins/text-editor-resources/src/components/ReferenceInput.svelte +++ b/plugins/text-editor-resources/src/components/ReferenceInput.svelte @@ -13,9 +13,9 @@ // limitations under the License. -->
@@ -156,13 +206,7 @@ {boundary} {canEmbedFiles} {canEmbedImages} - on:content={(ev) => { - if (canSubmit) { - dispatch('message', ev.detail) - content = EmptyMarkup - editor?.clear() - } - }} + on:content={handleSubmit} on:blur={() => { focused = false dispatch('blur') @@ -174,6 +218,7 @@ }} extensions={[ completionPlugin, + InlineCommandsExtension.configure(inlineCommandsConfig(allCommands, handleShortcut)), EmojiExtension.configure(), IsEmptyContentExtension.configure({ onChange: (value) => (isEmpty = value) }) ]} @@ -188,9 +233,10 @@ {#if showActions} {#each actions as a}