mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-26 19:44:57 +02:00
Merge branch 'develop' of https://github.com/hcengineering/platform into staging-new
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Vendored
+2
-1
@@ -694,7 +694,8 @@
|
||||
"ACCOUNTS_URL": "http://localhost:3000",
|
||||
"QUEUE_CONFIG": "localhost:19092",
|
||||
"QUEUE_REGION": "cockroach",
|
||||
"TEMPORAL_ADDRESS": "localhost:7233"
|
||||
"TEMPORAL_ADDRESS": "localhost:7233",
|
||||
"COLLABORATOR_URL": "ws://localhost:3078"
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"runtimeVersion": "20",
|
||||
|
||||
Generated
+570
-529
File diff suppressed because it is too large
Load Diff
@@ -283,7 +283,7 @@
|
||||
"electron-updater": "^6.3.4",
|
||||
"livekit-client": "^2.15.6",
|
||||
"@hcengineering/server-backup": "^0.7.0",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"productName": "Huly Desktop",
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
--huly-history-box-left-indent: 0;
|
||||
}
|
||||
|
||||
.history-box {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[data-theme="theme-dark"] {
|
||||
--bg-body: #1a1928;
|
||||
--bg-secondary: #323233;
|
||||
@@ -207,6 +211,15 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.desktop-app-control-button.desktop-app-back,
|
||||
.desktop-app-control-button.desktop-app-forward {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
padding-bottom: 2px;
|
||||
-webkit-text-stroke: 0.5px currentColor;
|
||||
}
|
||||
|
||||
.desktop-app-control-button:hover {
|
||||
background-color: var(--bg-hover);
|
||||
}
|
||||
@@ -229,6 +242,11 @@
|
||||
<div class="desktop-app-menu-container">
|
||||
</div>
|
||||
|
||||
<div class="desktop-app-window-controls">
|
||||
<button class="desktop-app-control-button desktop-app-back" id="back-button" title="Back">←</button>
|
||||
<button class="desktop-app-control-button desktop-app-forward" id="forward-button" title="Forward">→</button>
|
||||
</div>
|
||||
|
||||
<div class="desktop-app-window-title" id="application-title-bar-caption">Huly</div>
|
||||
|
||||
<div class="desktop-app-window-controls">
|
||||
|
||||
@@ -365,6 +365,14 @@ class MenuBarManager {
|
||||
ipcMain.closeWindow()
|
||||
})
|
||||
|
||||
this.onButtonClick('back-button', () => {
|
||||
history.back()
|
||||
})
|
||||
|
||||
this.onButtonClick('forward-button', () => {
|
||||
history.forward()
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', (e) => { this.handleKeyDown(ipcMain, e) })
|
||||
document.addEventListener('keyup', (e) => { this.handleKeyUp(e) })
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
"@hcengineering/api-client": "^0.7.5",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hcengineering/hulylake-client": "^0.7.6",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/pod-rating": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ import {
|
||||
TypeNumber,
|
||||
TypeRef,
|
||||
TypeString,
|
||||
UX
|
||||
UX,
|
||||
ReadOnly
|
||||
} from '@hcengineering/model'
|
||||
import attachment from '@hcengineering/model-attachment'
|
||||
import { TAttachedDoc, TClass, TDoc, TMixin, TSpace } from '@hcengineering/model-core'
|
||||
@@ -92,6 +93,11 @@ export class TTag extends TMixin implements Tag {
|
||||
@Model(card.class.Card, core.class.Doc, DOMAIN_CARD)
|
||||
@UX(card.string.Card, card.icon.Card)
|
||||
export class TCard extends TDoc implements Card {
|
||||
@Prop(TypeRef(card.class.CardSpace), core.string.Space)
|
||||
@Index(IndexKind.Indexed)
|
||||
@ReadOnly()
|
||||
declare space: Ref<CardSpace>
|
||||
|
||||
@Prop(TypeRef(card.class.MasterTag), card.string.MasterTag)
|
||||
declare _class: Ref<MasterTag>
|
||||
|
||||
@@ -129,6 +135,10 @@ export class TCard extends TDoc implements Card {
|
||||
children?: number
|
||||
|
||||
parentInfo!: ParentInfo[]
|
||||
|
||||
@Hidden()
|
||||
@ReadOnly()
|
||||
peerId?: string
|
||||
}
|
||||
|
||||
@Model(card.class.CardSpace, core.class.Space, DOMAIN_SPACE)
|
||||
|
||||
@@ -175,10 +175,24 @@ export function createModel (builder: Builder): void {
|
||||
attachTo: chunter.class.Channel,
|
||||
descriptor: view.viewlet.Table,
|
||||
configOptions: {
|
||||
strict: true
|
||||
hiddenKeys: ['name', 'description']
|
||||
},
|
||||
config: ['', 'topic', 'private', 'archived', 'members'],
|
||||
props: { enableChecking: false }
|
||||
props: { enableChecking: false },
|
||||
viewOptions: {
|
||||
groupBy: [],
|
||||
orderBy: [],
|
||||
other: [
|
||||
{
|
||||
key: 'hideArchived',
|
||||
type: 'toggle',
|
||||
defaultValue: true,
|
||||
actionTarget: 'options',
|
||||
action: view.function.HideArchived,
|
||||
label: view.string.HideArchived
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
chunter.viewlet.Channels
|
||||
)
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"@hcengineering/card": "^0.7.0",
|
||||
"@hcengineering/communication": "^0.7.0",
|
||||
"@hcengineering/communication-resources": "^0.7.0",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/contact": "^0.7.0",
|
||||
"@hcengineering/core": "^0.7.10",
|
||||
"@hcengineering/model": "^0.7.6",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { concatLink, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { BillingError, NetworkError } from './error'
|
||||
import {
|
||||
AiTokensData,
|
||||
AiTranscriptData,
|
||||
BillingStats,
|
||||
DatalakeStats,
|
||||
LiveKitEgressData,
|
||||
@@ -83,6 +85,29 @@ export class BillingClient {
|
||||
const body = JSON.stringify(egress)
|
||||
await fetchSafe(url, { method: 'POST', headers: { ...this.headers }, body })
|
||||
}
|
||||
|
||||
async getAiTranscriptLastData (): Promise<AiTranscriptData | undefined> {
|
||||
const path = '/api/v1/ai/transcript/last'
|
||||
const url = new URL(concatLink(this.endpoint, path))
|
||||
const response = await fetchSafe(url, { headers: { ...this.headers } })
|
||||
return (await response.json()) as AiTranscriptData | undefined
|
||||
}
|
||||
|
||||
async postAiTranscriptData (data: AiTranscriptData[]): Promise<void> {
|
||||
const path = '/api/v1/ai/transcript'
|
||||
const url = new URL(concatLink(this.endpoint, path))
|
||||
const body = JSON.stringify(data)
|
||||
|
||||
await fetchSafe(url, { method: 'POST', headers: { ...this.headers }, body })
|
||||
}
|
||||
|
||||
async postAiTokensData (data: AiTokensData[]): Promise<void> {
|
||||
const path = '/api/v1/ai/tokens'
|
||||
const url = new URL(concatLink(this.endpoint, path))
|
||||
const body = JSON.stringify(data)
|
||||
|
||||
await fetchSafe(url, { method: 'POST', headers: { ...this.headers }, body })
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSafe (url: string | URL, init?: RequestInit): Promise<Response> {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { WorkspaceUuid } from '@hcengineering/core'
|
||||
|
||||
export interface BillingStats {
|
||||
liveKitStats: LiveKitStats
|
||||
datalakeStats: DatalakeStats
|
||||
aiStats: AiStats
|
||||
}
|
||||
|
||||
export interface DatalakeStats {
|
||||
@@ -42,3 +45,33 @@ export interface LiveKitEgressData {
|
||||
egressEnd: string
|
||||
duration: number
|
||||
}
|
||||
|
||||
export interface AiTranscriptStats {
|
||||
totalDurationSeconds: number
|
||||
}
|
||||
|
||||
export interface AiTokensStats {
|
||||
reason: string
|
||||
totalTokens: number
|
||||
}
|
||||
|
||||
export interface AiStats {
|
||||
transcript: AiTranscriptStats
|
||||
tokens: AiTokensStats[]
|
||||
}
|
||||
|
||||
export interface AiTranscriptData {
|
||||
workspace: WorkspaceUuid
|
||||
day: string
|
||||
lastRequestId: string
|
||||
lastStartTime: string
|
||||
durationSeconds: number
|
||||
usd: number
|
||||
}
|
||||
|
||||
export interface AiTokensData {
|
||||
workspace: WorkspaceUuid
|
||||
reason: string
|
||||
tokens: number
|
||||
date: string
|
||||
}
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Zpět",
|
||||
"Redo": "Znovu",
|
||||
"ClearCanvas": "Vyčistit plátno",
|
||||
"PenTool": "Nástroj pero",
|
||||
"EraserTool": "Nástroj guma",
|
||||
"PanTool": "Nástroj posun",
|
||||
"TextTool": "Nástroj text",
|
||||
"PenTool": "Pero",
|
||||
"EraserTool": "Guma",
|
||||
"PanTool": "Posun",
|
||||
"TextTool": "Text",
|
||||
"LineTool": "Čára",
|
||||
"RectangleTool": "Obdélník",
|
||||
"EllipseTool": "Elipsa",
|
||||
"PaletteManagementMenu": "Spravovat barevné předvolby"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Rückgängig",
|
||||
"Redo": "Wiederholen",
|
||||
"ClearCanvas": "Leinwand löschen",
|
||||
"PenTool": "Stift-Werkzeug",
|
||||
"EraserTool": "Radiergummi-Werkzeug",
|
||||
"PanTool": "Verschieben-Werkzeug",
|
||||
"TextTool": "Text-Werkzeug",
|
||||
"PenTool": "Stift",
|
||||
"EraserTool": "Radiergummi",
|
||||
"PanTool": "Verschieben",
|
||||
"TextTool": "Text",
|
||||
"LineTool": "Linie",
|
||||
"RectangleTool": "Rechteck",
|
||||
"EllipseTool": "Ellipse",
|
||||
"PaletteManagementMenu": "Farbpresets verwalten"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Undo",
|
||||
"Redo": "Redo",
|
||||
"ClearCanvas": "Clear canvas",
|
||||
"PenTool": "Pen tool",
|
||||
"EraserTool": "Eraser tool",
|
||||
"PanTool": "Pan tool",
|
||||
"TextTool": "Text tool",
|
||||
"PenTool": "Pen",
|
||||
"EraserTool": "Eraser",
|
||||
"PanTool": "Pan",
|
||||
"TextTool": "Text",
|
||||
"LineTool": "Line",
|
||||
"RectangleTool": "Rectangle",
|
||||
"EllipseTool": "Ellipse",
|
||||
"PaletteManagementMenu": "Manage color presets"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Deshacer",
|
||||
"Redo": "Rehacer",
|
||||
"ClearCanvas": "Limpiar lienzo",
|
||||
"PenTool": "Herramienta lápiz",
|
||||
"EraserTool": "Herramienta borrador",
|
||||
"PanTool": "Herramienta mover",
|
||||
"TextTool": "Herramienta texto",
|
||||
"PenTool": "Lápiz",
|
||||
"EraserTool": "Borrador",
|
||||
"PanTool": "Mover",
|
||||
"TextTool": "Texto",
|
||||
"LineTool": "Línea",
|
||||
"RectangleTool": "Rectángulo",
|
||||
"EllipseTool": "Elipse",
|
||||
"PaletteManagementMenu": "Gestionar preajustes de color"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Annuler",
|
||||
"Redo": "Rétablir",
|
||||
"ClearCanvas": "Effacer la toile",
|
||||
"PenTool": "Outil stylo",
|
||||
"EraserTool": "Outil gomme",
|
||||
"PanTool": "Outil déplacement",
|
||||
"TextTool": "Outil texte",
|
||||
"PenTool": "Stylo",
|
||||
"EraserTool": "Gomme",
|
||||
"PanTool": "Déplacement",
|
||||
"TextTool": "Texte",
|
||||
"LineTool": "Ligne",
|
||||
"RectangleTool": "Rectangle",
|
||||
"EllipseTool": "Ellipse",
|
||||
"PaletteManagementMenu": "Gérer les préréglages de couleur"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Annulla",
|
||||
"Redo": "Ripristina",
|
||||
"ClearCanvas": "Cancella la tela",
|
||||
"PenTool": "Strumento penna",
|
||||
"EraserTool": "Strumento gomma",
|
||||
"PanTool": "Strumento sposta",
|
||||
"TextTool": "Strumento testo",
|
||||
"PenTool": "Penna",
|
||||
"EraserTool": "Gomma",
|
||||
"PanTool": "Sposta",
|
||||
"TextTool": "Testo",
|
||||
"LineTool": "Linea",
|
||||
"RectangleTool": "Rettangolo",
|
||||
"EllipseTool": "Ellisse",
|
||||
"PaletteManagementMenu": "Gestisci i preset di colore"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "元に戻す",
|
||||
"Redo": "やり直し",
|
||||
"ClearCanvas": "キャンバスをクリア",
|
||||
"PenTool": "ペンツール",
|
||||
"EraserTool": "消しゴムツール",
|
||||
"PanTool": "パンツール",
|
||||
"TextTool": "テキストツール",
|
||||
"PenTool": "ペン",
|
||||
"EraserTool": "消しゴム",
|
||||
"PanTool": "パン",
|
||||
"TextTool": "テキスト",
|
||||
"LineTool": "直線",
|
||||
"RectangleTool": "長方形",
|
||||
"EllipseTool": "楕円",
|
||||
"PaletteManagementMenu": "カラープリセットを管理"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Desfazer",
|
||||
"Redo": "Refazer",
|
||||
"ClearCanvas": "Limpar tela",
|
||||
"PenTool": "Ferramenta caneta",
|
||||
"EraserTool": "Ferramenta borracha",
|
||||
"PanTool": "Ferramenta mover",
|
||||
"TextTool": "Ferramenta texto",
|
||||
"PenTool": "Caneta",
|
||||
"EraserTool": "Borracha",
|
||||
"PanTool": "Mover",
|
||||
"TextTool": "Texto",
|
||||
"LineTool": "Linha",
|
||||
"RectangleTool": "Retângulo",
|
||||
"EllipseTool": "Elipse",
|
||||
"PaletteManagementMenu": "Gerenciar predefinições de cor"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Отменить",
|
||||
"Redo": "Повторить",
|
||||
"ClearCanvas": "Очистить холст",
|
||||
"PenTool": "Инструмент перо",
|
||||
"EraserTool": "Инструмент ластик",
|
||||
"PanTool": "Инструмент перемещения",
|
||||
"TextTool": "Инструмент текст",
|
||||
"PenTool": "Перо",
|
||||
"EraserTool": "Ластик",
|
||||
"PanTool": "Перемещение",
|
||||
"TextTool": "Текст",
|
||||
"LineTool": "Линия",
|
||||
"RectangleTool": "Прямоугольник",
|
||||
"EllipseTool": "Эллипс",
|
||||
"PaletteManagementMenu": "Управление цветовыми пресетами"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "Geri al",
|
||||
"Redo": "Yinele",
|
||||
"ClearCanvas": "Tuvali temizle",
|
||||
"PenTool": "Kalem aracı",
|
||||
"EraserTool": "Silgi aracı",
|
||||
"PanTool": "Kaydırma aracı",
|
||||
"TextTool": "Metin aracı",
|
||||
"PenTool": "Kalem",
|
||||
"EraserTool": "Silgi",
|
||||
"PanTool": "Kaydırma",
|
||||
"TextTool": "Metin",
|
||||
"LineTool": "Çizgi",
|
||||
"RectangleTool": "Dikdörtgen",
|
||||
"EllipseTool": "Elips",
|
||||
"PaletteManagementMenu": "Renk önayarlarını yönet"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -49,10 +49,13 @@
|
||||
"Undo": "撤销",
|
||||
"Redo": "重做",
|
||||
"ClearCanvas": "清除画布",
|
||||
"PenTool": "画笔工具",
|
||||
"EraserTool": "橡皮擦工具",
|
||||
"PanTool": "移动工具",
|
||||
"TextTool": "文字工具",
|
||||
"PenTool": "画笔",
|
||||
"EraserTool": "橡皮擦",
|
||||
"PanTool": "移动",
|
||||
"TextTool": "文字",
|
||||
"LineTool": "直线",
|
||||
"RectangleTool": "矩形",
|
||||
"EllipseTool": "椭圆",
|
||||
"PaletteManagementMenu": "管理颜色预设"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -46,10 +46,10 @@
|
||||
"@hcengineering/client": "^0.7.6",
|
||||
"@hcengineering/contact": "^0.7.0",
|
||||
"@hcengineering/collaborator-client": "^0.7.5",
|
||||
"@hcengineering/communication-client-query": "^0.7.7",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-shared": "^0.7.7",
|
||||
"@hcengineering/communication-client-query": "^0.7.8",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.9",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/communication-shared": "^0.7.8",
|
||||
"@hcengineering/core": "^0.7.10",
|
||||
"@hcengineering/diffview": "^0.7.0",
|
||||
"@hcengineering/notification": "^0.7.0",
|
||||
@@ -64,7 +64,7 @@
|
||||
"@hcengineering/retry": "^0.7.5",
|
||||
"@hcengineering/hulylake-client": "^0.7.6",
|
||||
"@hcengineering/hulypulse-client": "^0.7.0",
|
||||
"@hcengineering/storage-client": "^0.7.7",
|
||||
"@hcengineering/storage-client": "^0.7.8",
|
||||
"fast-equals": "^5.2.2",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"svelte": "^4.2.20",
|
||||
|
||||
@@ -26,10 +26,13 @@
|
||||
eventToHTMLElement,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { ComponentType, createEventDispatcher, onMount } from 'svelte'
|
||||
import IconEraser from './icons/Eraser.svelte'
|
||||
import IconMove from './icons/Move.svelte'
|
||||
import IconText from './icons/Text.svelte'
|
||||
import IconRectangle from './icons/Rectangle.svelte'
|
||||
import IconEllipse from './icons/Ellipse.svelte'
|
||||
import IconLine from './icons/Line.svelte'
|
||||
import { DrawingTool } from '../drawing'
|
||||
import presentation from '../plugin'
|
||||
import { ColorMetaName, ColorMetaNameOrHex } from '../drawingUtils'
|
||||
@@ -37,6 +40,23 @@
|
||||
import DrawingBoardColorSelectorIcon from './DrawingBoardColorSelectorIcon.svelte'
|
||||
import { ColorsList, DrawingBoardColoringSetup } from '../drawingColors'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
|
||||
interface ToolPresentation {
|
||||
label: IntlString
|
||||
icon: ComponentType
|
||||
tool: DrawingTool
|
||||
}
|
||||
|
||||
const tools: ToolPresentation[] = [
|
||||
{ label: presentation.string.PenTool, icon: IconEdit, tool: 'pen' },
|
||||
{ label: presentation.string.EraserTool, icon: IconEraser, tool: 'erase' },
|
||||
{ label: presentation.string.PanTool, icon: IconMove, tool: 'pan' },
|
||||
{ label: presentation.string.TextTool, icon: IconText, tool: 'text' },
|
||||
{ label: presentation.string.LineTool, icon: IconLine, tool: 'shape-line' },
|
||||
{ label: presentation.string.RectangleTool, icon: IconRectangle, tool: 'shape-rectangle' },
|
||||
{ label: presentation.string.EllipseTool, icon: IconEllipse, tool: 'shape-ellipse' }
|
||||
]
|
||||
|
||||
interface DrawingBoardToolbarEvents {
|
||||
undo: undefined
|
||||
@@ -45,6 +65,7 @@
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher<DrawingBoardToolbarEvents>()
|
||||
|
||||
const maxColors = 8
|
||||
const minColors = 0
|
||||
const defaultColor: ColorMetaName = 'alpha'
|
||||
@@ -58,6 +79,21 @@
|
||||
}
|
||||
|
||||
export let tool: DrawingTool = 'pen'
|
||||
|
||||
function evaluateToolPresentation (tool: DrawingTool): ToolPresentation {
|
||||
const found = tools.find((t) => t.tool === tool)
|
||||
if (found == null) {
|
||||
return tools[0]
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
let toolPresentation: ToolPresentation = evaluateToolPresentation(tool)
|
||||
|
||||
$: {
|
||||
toolPresentation = evaluateToolPresentation(tool)
|
||||
}
|
||||
|
||||
export let penColor: ColorMetaNameOrHex
|
||||
export let penWidth: number
|
||||
export let eraserWidth: number
|
||||
@@ -150,6 +186,25 @@
|
||||
localStorage.setItem(storageKey.color, penColor)
|
||||
}
|
||||
|
||||
function showToolSelectionMenu (ev: MouseEvent): void {
|
||||
const items: Array<Omit<SelectPopupValueType, 'id'> & { id: DrawingTool }> = []
|
||||
for (const toolPresentation of tools) {
|
||||
if (toolPresentation.tool === 'pan' && !showPanTool) {
|
||||
continue
|
||||
}
|
||||
items.push({
|
||||
id: toolPresentation.tool,
|
||||
label: toolPresentation.label,
|
||||
icon: toolPresentation.icon
|
||||
})
|
||||
}
|
||||
showPopup(SelectPopup, { value: items }, eventToHTMLElement(ev), (id: DrawingTool | undefined) => {
|
||||
if (id != null) {
|
||||
tool = id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
try {
|
||||
const savedColors = localStorage.getItem(storageKey.colors)
|
||||
@@ -210,61 +265,23 @@
|
||||
}}
|
||||
/>
|
||||
<div class="divider buttons-divider" />
|
||||
<Button kind="icon" showTooltip={{ label: toolPresentation.label }} noFocus on:click={showToolSelectionMenu}>
|
||||
<div class="tool-button-with-indicator" slot="content">
|
||||
<svelte:component this={toolPresentation.icon} size="small" />
|
||||
<div class="tool-indicator" />
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
icon={IconDelete}
|
||||
kind="icon"
|
||||
showTooltip={{ label: presentation.string.ClearCanvas }}
|
||||
noFocus
|
||||
on:click={() => {
|
||||
tool = 'pen'
|
||||
dispatch('clear')
|
||||
}}
|
||||
/>
|
||||
<div class="divider buttons-divider" />
|
||||
<Button
|
||||
icon={IconEdit}
|
||||
kind="icon"
|
||||
showTooltip={{ label: presentation.string.PenTool }}
|
||||
noFocus
|
||||
selected={tool === 'pen'}
|
||||
on:click={() => {
|
||||
tool = 'pen'
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={IconEraser}
|
||||
kind="icon"
|
||||
showTooltip={{ label: presentation.string.EraserTool }}
|
||||
noFocus
|
||||
selected={tool === 'erase'}
|
||||
on:click={() => {
|
||||
tool = 'erase'
|
||||
}}
|
||||
/>
|
||||
{#if showPanTool}
|
||||
<Button
|
||||
icon={IconMove}
|
||||
kind="icon"
|
||||
showTooltip={{ label: presentation.string.PanTool }}
|
||||
noFocus
|
||||
selected={tool === 'pan'}
|
||||
on:click={() => {
|
||||
tool = 'pan'
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<Button
|
||||
icon={IconText}
|
||||
kind="icon"
|
||||
showTooltip={{ label: presentation.string.TextTool }}
|
||||
noFocus
|
||||
selected={tool === 'text'}
|
||||
on:click={() => {
|
||||
tool = 'text'
|
||||
}}
|
||||
/>
|
||||
<div class="divider buttons-divider" />
|
||||
{#if tool === 'pen'}
|
||||
{#if tool !== 'erase' && tool !== 'pan' && tool !== 'text'}
|
||||
<input
|
||||
class="widthSelector"
|
||||
type="range"
|
||||
@@ -336,7 +353,12 @@
|
||||
&.inside {
|
||||
left: 0.5rem;
|
||||
top: 0.5rem;
|
||||
right: auto;
|
||||
bottom: unset;
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
max-width: calc(100% - 1rem);
|
||||
background-color: var(--theme-popup-header);
|
||||
border-radius: var(--small-BorderRadius);
|
||||
border: 1px solid var(--theme-popup-divider);
|
||||
@@ -360,4 +382,23 @@
|
||||
.widthSelector {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.tool-button-with-indicator {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tool-indicator {
|
||||
position: absolute;
|
||||
bottom: -0.125rem;
|
||||
right: -0.125rem;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 0 0.25rem 0.25rem;
|
||||
border-color: transparent transparent currentColor transparent;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
// Copyright © 2025 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="8" cy="8" rx="6" ry="4" stroke="currentColor" fill="none" stroke-width="1" />
|
||||
</svg>
|
||||
@@ -1,3 +1,18 @@
|
||||
<!--
|
||||
// Copyright © 2021 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
// Copyright © 2025 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="2" y1="14" x2="14" y2="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
||||
</svg>
|
||||
@@ -1,3 +1,18 @@
|
||||
<!--
|
||||
// Copyright © 2021 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
// Copyright © 2025 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 3h12v10H2V3zm1 1v8h10V4H3z" />
|
||||
</svg>
|
||||
@@ -1,3 +1,18 @@
|
||||
<!--
|
||||
// Copyright © 2025 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
|
||||
@@ -31,7 +31,16 @@ import {
|
||||
offsetCanvasPoint,
|
||||
type ColorMetaNameOrHex
|
||||
} from './drawingUtils'
|
||||
import { type DrawingCmd, type CommandUid, type DrawTextCmd, type DrawLineCmd, makeCommandUid } from './drawingCommand'
|
||||
import {
|
||||
type DrawingCmd,
|
||||
type CommandUid,
|
||||
type DrawTextCmd,
|
||||
type DrawLineCmd,
|
||||
type DrawRectCmd,
|
||||
type DrawEllipseCmd,
|
||||
type DrawStraightLineCmd,
|
||||
makeCommandUid
|
||||
} from './drawingCommand'
|
||||
import { type ColorsList, DrawingBoardColoringSetup, metaColorNameToHex } from './drawingColors'
|
||||
|
||||
export interface DrawingData {
|
||||
@@ -71,7 +80,7 @@ export interface DrawingProps {
|
||||
toolChanged?: (tool: DrawingTool) => void
|
||||
}
|
||||
|
||||
export type DrawingTool = 'pen' | 'erase' | 'pan' | 'text'
|
||||
export type DrawingTool = 'pen' | 'erase' | 'pan' | 'text' | 'shape-rectangle' | 'shape-ellipse' | 'shape-line'
|
||||
|
||||
const maxTextLength = 500
|
||||
|
||||
@@ -129,7 +138,13 @@ class DrawState {
|
||||
}
|
||||
|
||||
isDrawingTool = (): boolean => {
|
||||
return this.tool === 'pen' || this.tool === 'erase'
|
||||
return (
|
||||
this.tool === 'pen' ||
|
||||
this.tool === 'erase' ||
|
||||
this.tool === 'shape-rectangle' ||
|
||||
this.tool === 'shape-ellipse' ||
|
||||
this.tool === 'shape-line'
|
||||
)
|
||||
}
|
||||
|
||||
translateCtx = (): void => {
|
||||
@@ -165,6 +180,12 @@ class DrawState {
|
||||
drawCommand = (cmd: DrawingCmd, currentTheme: ThemeVariantType): void => {
|
||||
if (cmd.type === 'text') {
|
||||
this.drawTextCommand(cmd as DrawTextCmd, currentTheme)
|
||||
} else if (cmd.type === 'rectangle') {
|
||||
this.drawRectCommand(cmd as DrawRectCmd, currentTheme)
|
||||
} else if (cmd.type === 'ellipse') {
|
||||
this.drawEllipseCommand(cmd as DrawEllipseCmd, currentTheme)
|
||||
} else if (cmd.type === 'straight-line') {
|
||||
this.drawStraightLineCommand(cmd as DrawStraightLineCmd, currentTheme)
|
||||
} else {
|
||||
this.drawLineCommand(cmd as DrawLineCmd, currentTheme)
|
||||
}
|
||||
@@ -206,6 +227,46 @@ class DrawState {
|
||||
this.ctx.restore()
|
||||
}
|
||||
|
||||
drawRectCommand = (cmd: DrawRectCmd, currentTheme: ThemeVariantType): void => {
|
||||
this.ctx.save()
|
||||
this.translateCtx()
|
||||
this.ctx.beginPath()
|
||||
this.ctx.strokeStyle = metaColorNameToHex(cmd.penColor, currentTheme, this.colors)
|
||||
this.ctx.lineWidth = cmd.lineWidth
|
||||
const width = cmd.end.x - cmd.start.x
|
||||
const height = cmd.end.y - cmd.start.y
|
||||
this.ctx.strokeRect(cmd.start.x, cmd.start.y, width, height)
|
||||
this.ctx.restore()
|
||||
}
|
||||
|
||||
drawEllipseCommand = (cmd: DrawEllipseCmd, currentTheme: ThemeVariantType): void => {
|
||||
this.ctx.save()
|
||||
this.translateCtx()
|
||||
this.ctx.beginPath()
|
||||
this.ctx.strokeStyle = metaColorNameToHex(cmd.penColor, currentTheme, this.colors)
|
||||
this.ctx.lineWidth = cmd.lineWidth
|
||||
const centerX = (cmd.start.x + cmd.end.x) / 2
|
||||
const centerY = (cmd.start.y + cmd.end.y) / 2
|
||||
const radiusX = Math.abs(cmd.end.x - cmd.start.x) / 2
|
||||
const radiusY = Math.abs(cmd.end.y - cmd.start.y) / 2
|
||||
this.ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2)
|
||||
this.ctx.stroke()
|
||||
this.ctx.restore()
|
||||
}
|
||||
|
||||
drawStraightLineCommand = (cmd: DrawStraightLineCmd, currentTheme: ThemeVariantType): void => {
|
||||
this.ctx.save()
|
||||
this.translateCtx()
|
||||
this.ctx.beginPath()
|
||||
this.ctx.lineCap = 'round'
|
||||
this.ctx.strokeStyle = metaColorNameToHex(cmd.penColor, currentTheme, this.colors)
|
||||
this.ctx.lineWidth = cmd.lineWidth
|
||||
this.ctx.moveTo(cmd.start.x, cmd.start.y)
|
||||
this.ctx.lineTo(cmd.end.x, cmd.end.y)
|
||||
this.ctx.stroke()
|
||||
this.ctx.restore()
|
||||
}
|
||||
|
||||
isPointInText = (p: CanvasPoint, cmd: DrawTextCmd): boolean => {
|
||||
this.ctx.font = `${cmd.fontSize}px ${cmd.fontFace}`
|
||||
const lines = cmd.text.split('\n').map((l) => l.trim())
|
||||
@@ -311,7 +372,7 @@ export function drawing (
|
||||
draw.offset = props.offset ?? draw.offset
|
||||
let isOffsetAnimating = false
|
||||
|
||||
updateToolCursor()
|
||||
updateToolCursor(false)
|
||||
updateCanvasTouchAction()
|
||||
|
||||
interface LiveTextBox {
|
||||
@@ -338,7 +399,7 @@ export function drawing (
|
||||
replayCommands(currentCommands)
|
||||
|
||||
props.subscribeOnThemeChange(() => {
|
||||
updateToolCursor()
|
||||
updateToolCursor(false)
|
||||
replayCommands(currentCommands)
|
||||
})
|
||||
|
||||
@@ -431,14 +492,16 @@ export function drawing (
|
||||
|
||||
canvas.ontouchcancel = canvas.ontouchend
|
||||
|
||||
const MiddleMouseButton = 1
|
||||
|
||||
canvas.onpointerdown = (e) => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
const MiddleMouseButton = 1
|
||||
if (e.button === MiddleMouseButton && props.enableMiddleMousePanning === true) {
|
||||
e.preventDefault()
|
||||
isMiddleMousePanning = true
|
||||
updateToolCursor(true)
|
||||
canvas.setPointerCapture(e.pointerId)
|
||||
const forcePan = true
|
||||
drawStart(pointerToNodePoint(e), forcePan)
|
||||
@@ -470,8 +533,9 @@ export function drawing (
|
||||
canvas.releasePointerCapture(e.pointerId)
|
||||
const forcePan = isMiddleMousePanning
|
||||
drawEnd(pointerToNodePoint(e), forcePan)
|
||||
if (e.button === 1) {
|
||||
if (e.button === MiddleMouseButton) {
|
||||
isMiddleMousePanning = false
|
||||
updateToolCursor(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,6 +548,7 @@ export function drawing (
|
||||
const forcePan = isMiddleMousePanning
|
||||
drawEnd(pointerToNodePoint(e), forcePan)
|
||||
isMiddleMousePanning = false
|
||||
updateToolCursor(false)
|
||||
}
|
||||
|
||||
canvas.onpointerenter = () => {
|
||||
@@ -516,16 +581,33 @@ export function drawing (
|
||||
function drawContinue (p: NodePoint, forcePan: boolean): void {
|
||||
const scaledPoint = rescaleWithCss(p)
|
||||
|
||||
if (!forcePan && draw.isDrawingTool()) {
|
||||
if (draw.isDrawingTool() || forcePan) {
|
||||
const cursorSize = draw.cursorWidth()
|
||||
|
||||
const canvasOffsetInParent = offsetInParent(node, canvas)
|
||||
const parentRelativeLocation = offsetPoint(scaledPoint, canvasOffsetInParent)
|
||||
toolCursor.style.left = `${parentRelativeLocation.x - cursorSize / 2}px`
|
||||
toolCursor.style.top = `${parentRelativeLocation.y - cursorSize / 2}px`
|
||||
}
|
||||
|
||||
if (!forcePan && draw.isDrawingTool()) {
|
||||
if (draw.on) {
|
||||
if (Math.hypot(prevPos.x - scaledPoint.x, prevPos.y - scaledPoint.y) >= draw.minLineLength) {
|
||||
if (draw.tool === 'shape-rectangle') {
|
||||
requestAnimationFrame(() => {
|
||||
replayCommands(currentCommands)
|
||||
drawPreviewRectangle(scaledPoint)
|
||||
})
|
||||
} else if (draw.tool === 'shape-ellipse') {
|
||||
requestAnimationFrame(() => {
|
||||
replayCommands(currentCommands)
|
||||
drawPreviewEllipse(scaledPoint)
|
||||
})
|
||||
} else if (draw.tool === 'shape-line') {
|
||||
requestAnimationFrame(() => {
|
||||
replayCommands(currentCommands)
|
||||
drawPreviewStraightLine(scaledPoint)
|
||||
})
|
||||
} else if (Math.hypot(prevPos.x - scaledPoint.x, prevPos.y - scaledPoint.y) >= draw.minLineLength) {
|
||||
draw.drawLine(scaledPoint, 'intermediate-point', props.getCurrentTheme())
|
||||
prevPos = scaledPoint
|
||||
}
|
||||
@@ -548,7 +630,7 @@ export function drawing (
|
||||
prevPos = scaledPoint
|
||||
}
|
||||
|
||||
if (props.pointerMoved !== undefined && !forcePan) {
|
||||
if (props.pointerMoved !== undefined) {
|
||||
props.pointerMoved(draw.mouseToCanvasPoint(scaledPoint))
|
||||
}
|
||||
}
|
||||
@@ -557,8 +639,16 @@ export function drawing (
|
||||
const scaledPoint = rescaleWithCss(p)
|
||||
if (draw.on) {
|
||||
if (!forcePan && draw.isDrawingTool()) {
|
||||
draw.drawLine(scaledPoint, 'last-point', props.getCurrentTheme())
|
||||
storeLineCommand()
|
||||
if (draw.tool === 'shape-rectangle') {
|
||||
storeRectCommand(scaledPoint)
|
||||
} else if (draw.tool === 'shape-ellipse') {
|
||||
storeEllipseCommand(scaledPoint)
|
||||
} else if (draw.tool === 'shape-line') {
|
||||
storeStraightLineCommand(scaledPoint)
|
||||
} else {
|
||||
draw.drawLine(scaledPoint, 'last-point', props.getCurrentTheme())
|
||||
storeLineCommand()
|
||||
}
|
||||
} else if (draw.tool === 'pan' || forcePan) {
|
||||
props.panned?.(draw.offset)
|
||||
} else if (draw.tool === 'text') {
|
||||
@@ -885,24 +975,111 @@ export function drawing (
|
||||
}
|
||||
|
||||
function storeLineCommand (): void {
|
||||
if (draw.points.length > 0) {
|
||||
const erasing = draw.tool === 'erase'
|
||||
const cmd: DrawLineCmd = {
|
||||
if (draw.points.length === 0) {
|
||||
return
|
||||
}
|
||||
const erasing = draw.tool === 'erase'
|
||||
const cmd: DrawLineCmd = {
|
||||
id: makeCommandUid(),
|
||||
type: 'line',
|
||||
lineWidth: erasing ? draw.eraserWidth : draw.penWidth,
|
||||
erasing,
|
||||
penColor: draw.penColor,
|
||||
points: draw.points
|
||||
}
|
||||
props.cmdAdded?.(cmd)
|
||||
}
|
||||
|
||||
function drawShapePreview (
|
||||
endPoint: MouseScaledPoint,
|
||||
drawShape: (start: CanvasPoint, end: CanvasPoint) => void
|
||||
): void {
|
||||
if (draw.points.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const start = draw.points[0]
|
||||
const end = draw.mouseToCanvasPoint(endPoint)
|
||||
|
||||
draw.ctx.save()
|
||||
draw.translateCtx()
|
||||
draw.ctx.beginPath()
|
||||
draw.ctx.strokeStyle = metaColorNameToHex(draw.penColor, props.getCurrentTheme(), colorsSetup)
|
||||
draw.ctx.lineWidth = draw.penWidth
|
||||
drawShape(start, end)
|
||||
draw.ctx.stroke()
|
||||
draw.ctx.restore()
|
||||
}
|
||||
|
||||
function drawPreviewRectangle (endPoint: MouseScaledPoint): void {
|
||||
drawShapePreview(endPoint, (start, end) => {
|
||||
const width = end.x - start.x
|
||||
const height = end.y - start.y
|
||||
draw.ctx.strokeRect(start.x, start.y, width, height)
|
||||
})
|
||||
}
|
||||
|
||||
function drawPreviewEllipse (endPoint: MouseScaledPoint): void {
|
||||
drawShapePreview(endPoint, (start, end) => {
|
||||
const centerX = (start.x + end.x) / 2
|
||||
const centerY = (start.y + end.y) / 2
|
||||
const radiusX = Math.abs(end.x - start.x) / 2
|
||||
const radiusY = Math.abs(end.y - start.y) / 2
|
||||
draw.ctx.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2)
|
||||
})
|
||||
}
|
||||
|
||||
function drawPreviewStraightLine (endPoint: MouseScaledPoint): void {
|
||||
drawShapePreview(endPoint, (start, end) => {
|
||||
draw.ctx.lineCap = 'round'
|
||||
draw.ctx.moveTo(start.x, start.y)
|
||||
draw.ctx.lineTo(end.x, end.y)
|
||||
})
|
||||
}
|
||||
|
||||
function storeShapeCommand (endPoint: MouseScaledPoint, type: 'rectangle' | 'ellipse' | 'straight-line'): void {
|
||||
if (draw.points.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const start = draw.points[0]
|
||||
const end = draw.mouseToCanvasPoint(endPoint)
|
||||
|
||||
const minSize = 2
|
||||
|
||||
const nonDegenerate = Math.abs(end.x - start.x) > minSize || Math.abs(end.y - start.y) > minSize
|
||||
if (nonDegenerate) {
|
||||
const cmd: DrawRectCmd | DrawEllipseCmd | DrawStraightLineCmd = {
|
||||
id: makeCommandUid(),
|
||||
type: 'line',
|
||||
lineWidth: erasing ? draw.eraserWidth : draw.penWidth,
|
||||
erasing,
|
||||
type,
|
||||
lineWidth: draw.penWidth,
|
||||
penColor: draw.penColor,
|
||||
points: draw.points
|
||||
start,
|
||||
end
|
||||
}
|
||||
props.cmdAdded?.(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
function updateToolCursor (): void {
|
||||
function storeRectCommand (endPoint: MouseScaledPoint): void {
|
||||
storeShapeCommand(endPoint, 'rectangle')
|
||||
}
|
||||
|
||||
function storeEllipseCommand (endPoint: MouseScaledPoint): void {
|
||||
storeShapeCommand(endPoint, 'ellipse')
|
||||
}
|
||||
|
||||
function storeStraightLineCommand (endPoint: MouseScaledPoint): void {
|
||||
storeShapeCommand(endPoint, 'straight-line')
|
||||
}
|
||||
|
||||
function updateToolCursor (forcePanning: boolean): void {
|
||||
if (readonly) {
|
||||
toolCursor.style.visibility = 'hidden'
|
||||
canvas.style.cursor = props.defaultCursor ?? 'default'
|
||||
} else if (forcePanning) {
|
||||
canvas.style.cursor = 'grabbing'
|
||||
toolCursor.style.visibility = 'hidden'
|
||||
} else if (draw.isDrawingTool()) {
|
||||
canvas.style.cursor = 'none'
|
||||
toolCursor.style.visibility = 'visible'
|
||||
@@ -1128,7 +1305,7 @@ export function drawing (
|
||||
props.toolChanged?.(draw.tool)
|
||||
}
|
||||
if (syncToolCursor) {
|
||||
updateToolCursor()
|
||||
updateToolCursor(false)
|
||||
}
|
||||
if (syncPersonCursor !== undefined) {
|
||||
updatePersonCursor(syncPersonCursor)
|
||||
|
||||
@@ -20,7 +20,7 @@ export type CommandUid = string & { readonly __brand: 'CommandUid' }
|
||||
|
||||
export interface DrawingCmd {
|
||||
id: CommandUid
|
||||
type: 'line' | 'text'
|
||||
type: 'line' | 'text' | 'rectangle' | 'ellipse' | 'straight-line'
|
||||
}
|
||||
|
||||
export interface DrawTextCmd extends DrawingCmd {
|
||||
@@ -38,6 +38,27 @@ export interface DrawLineCmd extends DrawingCmd {
|
||||
points: CanvasPoint[]
|
||||
}
|
||||
|
||||
export interface DrawRectCmd extends DrawingCmd {
|
||||
lineWidth: number
|
||||
penColor: ColorMetaNameOrHex
|
||||
start: CanvasPoint
|
||||
end: CanvasPoint
|
||||
}
|
||||
|
||||
export interface DrawEllipseCmd extends DrawingCmd {
|
||||
lineWidth: number
|
||||
penColor: ColorMetaNameOrHex
|
||||
start: CanvasPoint
|
||||
end: CanvasPoint
|
||||
}
|
||||
|
||||
export interface DrawStraightLineCmd extends DrawingCmd {
|
||||
lineWidth: number
|
||||
penColor: ColorMetaNameOrHex
|
||||
start: CanvasPoint
|
||||
end: CanvasPoint
|
||||
}
|
||||
|
||||
export const makeCommandUid = (): CommandUid => {
|
||||
return (crypto?.randomUUID?.() ?? generateId()) as CommandUid
|
||||
}
|
||||
|
||||
@@ -155,6 +155,9 @@ export default plugin(presentationId, {
|
||||
EraserTool: '' as IntlString,
|
||||
PanTool: '' as IntlString,
|
||||
TextTool: '' as IntlString,
|
||||
LineTool: '' as IntlString,
|
||||
RectangleTool: '' as IntlString,
|
||||
EllipseTool: '' as IntlString,
|
||||
PaletteManagementMenu: '' as IntlString
|
||||
},
|
||||
extension: {
|
||||
|
||||
@@ -15,24 +15,50 @@ import { HulypulseClient } from '@hcengineering/hulypulse-client'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import presentation from './plugin'
|
||||
|
||||
let pulseClient: HulypulseClient | undefined
|
||||
let currentWorkspaceUuid: string | undefined
|
||||
let currentToken: string | undefined
|
||||
let promise: Promise<HulypulseClient | undefined> | undefined
|
||||
|
||||
export async function createPulseClient (): Promise<HulypulseClient | undefined> {
|
||||
const token = getMetadata(presentation.metadata.Token)
|
||||
if (token !== currentToken) {
|
||||
const pulseUrl = getMetadata(presentation.metadata.PulseUrl) ?? ''
|
||||
const token = getMetadata(presentation.metadata.Token) ?? ''
|
||||
const workspaceUuid = getMetadata(presentation.metadata.WorkspaceUuid) ?? ''
|
||||
|
||||
if (pulseUrl === '' || token === '' || workspaceUuid === '') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Token or workspace changed, need to reconnect
|
||||
if (token !== currentToken || workspaceUuid !== currentWorkspaceUuid) {
|
||||
closePulseClient()
|
||||
}
|
||||
if (pulseClient === undefined) {
|
||||
const wsPulseUrl = getMetadata(presentation.metadata.PulseUrl)
|
||||
if (wsPulseUrl == null || wsPulseUrl.trim().length === 0) return undefined
|
||||
pulseClient = await HulypulseClient.connect(`${wsPulseUrl}?token=${token}`)
|
||||
currentToken = token
|
||||
|
||||
if (promise !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/return-await
|
||||
return promise
|
||||
}
|
||||
return pulseClient
|
||||
|
||||
promise = new Promise((resolve) => {
|
||||
HulypulseClient.connect(`${pulseUrl}?token=${token}`)
|
||||
.then(resolve)
|
||||
.catch(() => {
|
||||
resolve(undefined)
|
||||
})
|
||||
})
|
||||
currentToken = token
|
||||
currentWorkspaceUuid = workspaceUuid
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/return-await
|
||||
return promise
|
||||
}
|
||||
|
||||
export function closePulseClient (): void {
|
||||
pulseClient?.close()
|
||||
pulseClient = undefined
|
||||
if (promise !== undefined) {
|
||||
void promise.then((client) => {
|
||||
client?.close()
|
||||
})
|
||||
}
|
||||
promise = undefined
|
||||
currentToken = undefined
|
||||
currentWorkspaceUuid = undefined
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { location as locationStore } from '../../location'
|
||||
import { location as locationStore, workspaceId } from '../../location'
|
||||
import { rootBarExtensions } from '../../utils'
|
||||
import Component from '../Component.svelte'
|
||||
|
||||
@@ -15,18 +15,20 @@
|
||||
$: sorted = $rootBarExtensions.sort((a, b) => a[1].order - b[1].order)
|
||||
</script>
|
||||
|
||||
{#each sorted as ext (ext[1].id)}
|
||||
{#if ext[0] === position}
|
||||
<div id={ext[1].id} class="clear-mins">
|
||||
<Component
|
||||
is={ext[1].component}
|
||||
props={ext[1].props}
|
||||
on:close={() => {
|
||||
rootBarExtensions.update((cur) => {
|
||||
return cur.filter((it) => it[1].id !== ext[1].id)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#key $workspaceId}
|
||||
{#each sorted as ext (ext[1].id)}
|
||||
{#if ext[0] === position}
|
||||
<div id={ext[1].id} class="clear-mins">
|
||||
<Component
|
||||
is={ext[1].component}
|
||||
props={ext[1].props}
|
||||
on:close={() => {
|
||||
rootBarExtensions.update((cur) => {
|
||||
return cur.filter((it) => it[1].id !== ext[1].id)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/key}
|
||||
|
||||
@@ -355,6 +355,14 @@ export async function formatDuration (duration: number, language: string): Promi
|
||||
return text
|
||||
}
|
||||
|
||||
export function formatNumberCompact (num: number, maximumFractionDigits = 2): string {
|
||||
const locale = new Intl.NumberFormat().resolvedOptions().locale
|
||||
return new Intl.NumberFormat(locale, {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits
|
||||
}).format(num)
|
||||
}
|
||||
|
||||
export function pushRootBarComponent (pos: 'left' | 'right', component: AnyComponent, order?: number): void {
|
||||
rootBarExtensions.update((cur) => {
|
||||
if (cur.find((p) => p[1].component === component) === undefined) {
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"@hcengineering/ui": "^0.7.0",
|
||||
"@hcengineering/view": "^0.7.0",
|
||||
"@hcengineering/view-resources": "^0.7.0",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/emoji": "^0.7.0",
|
||||
"@hcengineering/emoji-resources": "^0.7.0",
|
||||
"svelte": "^4.2.20"
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "Počet souborů",
|
||||
"OfficeSessionsDuration": "Doba schůzek",
|
||||
"OfficeSessionsBandwidth": "Šířka pásma schůzek",
|
||||
"OfficeEgressDuration": "Doba záznamu"
|
||||
"OfficeEgressDuration": "Doba záznamu",
|
||||
"AI": "AI",
|
||||
"TranscriptionTime": "Doba přepisu",
|
||||
"TotalTokens": "Celkem tokenů"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "Dateianzahl",
|
||||
"OfficeSessionsDuration": "Besprechungsdauer",
|
||||
"OfficeSessionsBandwidth": "Besprechungsbandbreite",
|
||||
"OfficeEgressDuration": "Aufzeichnungsdauer"
|
||||
"OfficeEgressDuration": "Aufzeichnungsdauer",
|
||||
"AI": "AI",
|
||||
"TranscriptionTime": "Transkriptionszeit",
|
||||
"TotalTokens": "Gesamte Tokens"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "Files count",
|
||||
"OfficeSessionsDuration": "Meetings time",
|
||||
"OfficeSessionsBandwidth": "Meetings bandwidth",
|
||||
"OfficeEgressDuration": "Recording time"
|
||||
"OfficeEgressDuration": "Recording time",
|
||||
"AI": "AI",
|
||||
"TranscriptionTime": "Transcription time",
|
||||
"TotalTokens": "Total tokens"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "Cantidad de archivos",
|
||||
"OfficeSessionsDuration": "Tiempo de reuniones",
|
||||
"OfficeSessionsBandwidth": "Ancho de banda de reuniones",
|
||||
"OfficeEgressDuration": "Tiempo de grabación"
|
||||
"OfficeEgressDuration": "Tiempo de grabación",
|
||||
"AI": "IA",
|
||||
"TranscriptionTime": "Tiempo de transcripción",
|
||||
"TotalTokens": "Total de tokens"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "Nombre de fichiers",
|
||||
"OfficeSessionsDuration": "Durée des réunions",
|
||||
"OfficeSessionsBandwidth": "Bande passante des réunions",
|
||||
"OfficeEgressDuration": "Durée d'enregistrement"
|
||||
"OfficeEgressDuration": "Durée d'enregistrement",
|
||||
"AI": "IA",
|
||||
"TranscriptionTime": "Durée de transcription",
|
||||
"TotalTokens": "Nombre total de jetons"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "Conteggio file",
|
||||
"OfficeSessionsDuration": "Durata riunioni",
|
||||
"OfficeSessionsBandwidth": "Larghezza di banda riunioni",
|
||||
"OfficeEgressDuration": "Durata registrazione"
|
||||
"OfficeEgressDuration": "Durata registrazione",
|
||||
"AI": "AI",
|
||||
"TranscriptionTime": "Tempo di trascrizione",
|
||||
"TotalTokens": "Totale token"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "ファイル数",
|
||||
"OfficeSessionsDuration": "会議時間",
|
||||
"OfficeSessionsBandwidth": "会議帯域幅",
|
||||
"OfficeEgressDuration": "録画時間"
|
||||
"OfficeEgressDuration": "録画時間",
|
||||
"AI": "AI",
|
||||
"TranscriptionTime": "文字起こし時間",
|
||||
"TotalTokens": "合計トークン"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "Quantidade de arquivos",
|
||||
"OfficeSessionsDuration": "Tempo de reuniões",
|
||||
"OfficeSessionsBandwidth": "Largura de banda de reuniões",
|
||||
"OfficeEgressDuration": "Tempo de gravação"
|
||||
"OfficeEgressDuration": "Tempo de gravação",
|
||||
"AI": "IA",
|
||||
"TranscriptionTime": "Tempo de transcrição",
|
||||
"TotalTokens": "Total de tokens"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "Количество файлов",
|
||||
"OfficeSessionsDuration": "Время встреч",
|
||||
"OfficeSessionsBandwidth": "Трафик встреч",
|
||||
"OfficeEgressDuration": "Время записи"
|
||||
"OfficeEgressDuration": "Время записи",
|
||||
"AI": "AI",
|
||||
"TranscriptionTime": "Время транскрипции",
|
||||
"TotalTokens": "Всего токенов"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "Dosya sayısı",
|
||||
"OfficeSessionsDuration": "Toplantı süresi",
|
||||
"OfficeSessionsBandwidth": "Toplantı bant genişliği",
|
||||
"OfficeEgressDuration": "Kayıt süresi"
|
||||
"OfficeEgressDuration": "Kayıt süresi",
|
||||
"AI": "AI",
|
||||
"TranscriptionTime": "Transkripsiyon süresi",
|
||||
"TotalTokens": "Toplam belirteçler"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"DriveCount": "文件数量",
|
||||
"OfficeSessionsDuration": "会议时间",
|
||||
"OfficeSessionsBandwidth": "会议带宽",
|
||||
"OfficeEgressDuration": "录制时间"
|
||||
"OfficeEgressDuration": "录制时间",
|
||||
"AI": "AI",
|
||||
"TranscriptionTime": "转录时间",
|
||||
"TotalTokens": "总令牌数"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,15 +13,25 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getBillingClient } from '../utils'
|
||||
import { Breadcrumb, Header, Loading, Scroller, formatDuration, themeStore } from '@hcengineering/ui'
|
||||
import {
|
||||
Breadcrumb,
|
||||
Header,
|
||||
Loading,
|
||||
Scroller,
|
||||
formatDuration,
|
||||
themeStore,
|
||||
formatNumberCompact
|
||||
} from '@hcengineering/ui'
|
||||
import { getCurrentWorkspaceUuid } from '@hcengineering/presentation'
|
||||
import billingPlugin from '@hcengineering/billing'
|
||||
import filesize from 'filesize'
|
||||
import StatsCard from './StatsCard.svelte'
|
||||
import drivePlugin from '@hcengineering/drive'
|
||||
import Category from './Category.svelte'
|
||||
import love from '@hcengineering/love'
|
||||
import drivePlugin from '@hcengineering/drive'
|
||||
import view from '@hcengineering/view'
|
||||
|
||||
import { getBillingClient } from '../utils'
|
||||
import StatsCard from './StatsCard.svelte'
|
||||
import Category from './Category.svelte'
|
||||
import ChartCard from './ChartCard.svelte'
|
||||
|
||||
const billingClient = getBillingClient()
|
||||
@@ -34,6 +44,8 @@
|
||||
let sessionsDurationByDay: { date: number, value: number }[] = []
|
||||
let sessionsBandwidthByDay: { date: number, value: number }[] = []
|
||||
let egressDurationByDay: { date: number, value: number }[] = []
|
||||
let totalTranscriptDuration = 0
|
||||
let totalTokensCount = 0
|
||||
|
||||
async function loadBillingData (): Promise<void> {
|
||||
if (billingClient == null) return
|
||||
@@ -43,6 +55,8 @@
|
||||
totalSessionsDuration = billingStats.liveKitStats.sessions.reduce((sum, s) => sum + s.minutes, 0) * 60000
|
||||
totalSessionsBandwidth = billingStats.liveKitStats.sessions.reduce((sum, s) => sum + s.bandwidth, 0)
|
||||
totalEgressDuration = billingStats.liveKitStats.egress.reduce((sum, e) => sum + e.minutes, 0) * 60000
|
||||
totalTranscriptDuration = billingStats.aiStats.transcript.totalDurationSeconds * 1000
|
||||
totalTokensCount = billingStats.aiStats.tokens.reduce((sum, s) => sum + s.totalTokens, 0)
|
||||
|
||||
sessionsDurationByDay = billingStats.liveKitStats.sessions.map((s) => {
|
||||
const date = new Date(Date.parse(s.day))
|
||||
@@ -80,6 +94,15 @@
|
||||
<StatsCard label={billingPlugin.string.DriveCount} text={totalDatalakeCount.toString()} />
|
||||
</div>
|
||||
</Category>
|
||||
<Category icon={view.icon.AiStar} label={billingPlugin.string.AI}>
|
||||
<div class="row">
|
||||
<StatsCard
|
||||
label={billingPlugin.string.TranscriptionTime}
|
||||
text={formatDuration(totalTranscriptDuration, $themeStore.language)}
|
||||
/>
|
||||
<StatsCard label={billingPlugin.string.TotalTokens} text={formatNumberCompact(totalTokensCount)} />
|
||||
</div>
|
||||
</Category>
|
||||
<Category icon={love.icon.Love} label={love.string.Office}>
|
||||
<div class="row">
|
||||
<StatsCard
|
||||
|
||||
@@ -31,7 +31,10 @@ export const billingPlugin = plugin(billingId, {
|
||||
DriveCount: '' as IntlString,
|
||||
OfficeSessionsDuration: '' as IntlString,
|
||||
OfficeSessionsBandwidth: '' as IntlString,
|
||||
OfficeEgressDuration: '' as IntlString
|
||||
OfficeEgressDuration: '' as IntlString,
|
||||
AI: '' as IntlString,
|
||||
TotalTokens: '' as IntlString,
|
||||
TranscriptionTime: '' as IntlString
|
||||
},
|
||||
icon: {
|
||||
Billing: '' as Asset
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"dependencies": {
|
||||
"@hcengineering/presence": "^0.7.0",
|
||||
"@hcengineering/presentation": "^0.7.0",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/communication-resources": "^0.7.0",
|
||||
"@hcengineering/core": "^0.7.10",
|
||||
"@hcengineering/ui": "^0.7.0",
|
||||
|
||||
@@ -200,8 +200,14 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="presence">
|
||||
<Component is={presence.component.Presence} props={{ object: doc }} />
|
||||
<Component is={presence.component.PresenceAvatars} props={{ object: doc, size: 'x-small', limit: 5 }} />
|
||||
<Component
|
||||
is={presence.component.Presence}
|
||||
props={{ object: doc, presenceId: doc.peerId ? `peer:${doc.peerId}` : doc._id }}
|
||||
/>
|
||||
<Component
|
||||
is={presence.component.PresenceAvatars}
|
||||
props={{ object: doc, size: 'x-small', limit: 5, presenceId: doc.peerId ? `peer:${doc.peerId}` : doc._id }}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="pre-utils">
|
||||
|
||||
@@ -57,6 +57,8 @@ export interface Card extends Doc, IconProps {
|
||||
parent?: Ref<Card> | null
|
||||
rank: Rank
|
||||
readonly?: boolean
|
||||
|
||||
peerId?: string
|
||||
}
|
||||
|
||||
export interface CardSpace extends Space {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"@hcengineering/card": "^0.7.0",
|
||||
"@hcengineering/card-resources": "^0.7.0",
|
||||
"@hcengineering/chat": "^0.7.0",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/communication": "^0.7.0",
|
||||
"@hcengineering/contact": "^0.7.0",
|
||||
"@hcengineering/contact-resources": "^0.7.0",
|
||||
@@ -55,7 +55,7 @@
|
||||
"@hcengineering/workbench": "^0.7.0",
|
||||
"@hcengineering/workbench-resources": "^0.7.0",
|
||||
"@hcengineering/communication-resources": "^0.7.0",
|
||||
"@hcengineering/communication-shared": "^0.7.7",
|
||||
"@hcengineering/communication-shared": "^0.7.8",
|
||||
"@hcengineering/rank": "^0.7.5",
|
||||
"@hcengineering/text": "^0.7.5",
|
||||
"@hcengineering/text-markdown": "^0.7.5",
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<script lang="ts">
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import { type Doc, type PersonId, getCurrentAccount } from '@hcengineering/core'
|
||||
import { getName, getPersonRefsBySocialIds } from '@hcengineering/contact'
|
||||
import { getPersonsByPersonRefs } from '@hcengineering/contact-resources'
|
||||
import { getName } from '@hcengineering/contact'
|
||||
import { getPersonsByPersonIds } from '@hcengineering/contact-resources'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
@@ -26,7 +26,6 @@
|
||||
|
||||
const maxTypingPersons = 3
|
||||
const acc = getCurrentAccount()
|
||||
const client = getClient()
|
||||
const hierarchy = getClient().getHierarchy()
|
||||
|
||||
interface TypingGroup {
|
||||
@@ -58,21 +57,22 @@
|
||||
const groups: TypingGroup[] = []
|
||||
|
||||
for (const [status, personIds] of groupedByStatus.entries()) {
|
||||
const personRefs = await getPersonRefsBySocialIds(client, personIds)
|
||||
const persons = await getPersonsByPersonRefs(Object.values(personRefs))
|
||||
const persons = await getPersonsByPersonIds(personIds)
|
||||
const names = Array.from(persons.values())
|
||||
.map((person) => getName(hierarchy, person))
|
||||
.sort((name1, name2) => name1.localeCompare(name2))
|
||||
|
||||
const displayNames = names.slice(0, maxTypingPersons).join(', ')
|
||||
const moreCount = Math.max(names.length - maxTypingPersons, 0)
|
||||
if (names.length > 0) {
|
||||
const displayNames = names.slice(0, maxTypingPersons).join(', ')
|
||||
const moreCount = Math.max(names.length - maxTypingPersons, 0)
|
||||
|
||||
groups.push({
|
||||
status,
|
||||
names: displayNames,
|
||||
count: names.length,
|
||||
moreCount
|
||||
})
|
||||
groups.push({
|
||||
status,
|
||||
names: displayNames,
|
||||
count: names.length,
|
||||
moreCount
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
groups.sort((a, b) => a.status.localeCompare(b.status))
|
||||
|
||||
@@ -43,14 +43,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/analytics": "^0.7.5",
|
||||
"@hcengineering/activity": "^0.7.0",
|
||||
"@hcengineering/ai-bot": "^0.7.0",
|
||||
"@hcengineering/ai-bot-resources": "^0.7.0",
|
||||
"@hcengineering/attachment-resources": "^0.7.0",
|
||||
"@hcengineering/card": "^0.7.0",
|
||||
"@hcengineering/chat": "^0.7.0",
|
||||
"@hcengineering/communication": "^0.7.0",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-shared": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/communication-shared": "^0.7.8",
|
||||
"@hcengineering/contact": "^0.7.0",
|
||||
"@hcengineering/contact-resources": "^0.7.0",
|
||||
"@hcengineering/core": "^0.7.10",
|
||||
|
||||
@@ -12,17 +12,18 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import core, { type Class, type Client, type Doc, type Mixin, type Ref } from '@hcengineering/core'
|
||||
import view, { type AttributeModel } from '@hcengineering/view'
|
||||
import { buildRemovedDoc, getAttributePresenter } from '@hcengineering/view-resources'
|
||||
import { type Card } from '@hcengineering/card'
|
||||
import {
|
||||
type ActivityAttributeUpdate,
|
||||
type ActivityMessage,
|
||||
type ActivityUpdate,
|
||||
ActivityUpdateType,
|
||||
type Message,
|
||||
MessageType
|
||||
} from '@hcengineering/communication-types'
|
||||
import core, { type Class, type Client, type Doc, type Mixin, type Ref } from '@hcengineering/core'
|
||||
import view, { type AttributeModel } from '@hcengineering/view'
|
||||
import { buildRemovedDoc, getAttributePresenter } from '@hcengineering/view-resources'
|
||||
|
||||
const valueTypes: ReadonlyArray<Ref<Class<Doc>>> = [
|
||||
core.class.TypeString,
|
||||
@@ -52,13 +53,18 @@ export async function getAttributeModel (
|
||||
update: ActivityUpdate | undefined,
|
||||
_class: Ref<Class<Card>>
|
||||
): Promise<AttributeModel | undefined> {
|
||||
if (update == null || update.type !== ActivityUpdateType.Attribute) return undefined
|
||||
if (
|
||||
update == null ||
|
||||
(update.type !== ActivityUpdateType.Attribute && update.type !== ActivityUpdateType.CollaborativeChange)
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { attrKey } = update
|
||||
|
||||
const model = await getAttributePresenterSafe(
|
||||
client,
|
||||
update.mixin ?? _class,
|
||||
(update as ActivityAttributeUpdate).mixin ?? _class,
|
||||
attrKey,
|
||||
view.mixin.ActivityAttributePresenter
|
||||
)
|
||||
@@ -67,7 +73,7 @@ export async function getAttributeModel (
|
||||
return model
|
||||
}
|
||||
|
||||
return await getAttributePresenterSafe(client, update.mixin ?? _class, attrKey)
|
||||
return await getAttributePresenterSafe(client, (update as ActivityAttributeUpdate).mixin ?? _class, attrKey)
|
||||
}
|
||||
|
||||
export async function getAttributeValues (
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type PersonId, getCurrentAccount } from '@hcengineering/core'
|
||||
import { getName, getPersonRefsBySocialIds } from '@hcengineering/contact'
|
||||
import { getPersonsByPersonRefs } from '@hcengineering/contact-resources'
|
||||
import { getName } from '@hcengineering/contact'
|
||||
import { getPersonsByPersonIds } from '@hcengineering/contact-resources'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Label } from '@hcengineering/ui'
|
||||
@@ -25,10 +25,10 @@
|
||||
import communication from '../plugin'
|
||||
|
||||
export let cardId: CardID
|
||||
export let peerId: string | undefined
|
||||
|
||||
const maxTypingPersons = 3
|
||||
const acc = getCurrentAccount()
|
||||
const client = getClient()
|
||||
const hierarchy = getClient().getHierarchy()
|
||||
|
||||
interface TypingGroup {
|
||||
@@ -41,6 +41,8 @@
|
||||
let typingInfo = new Map<string, TypingInfo>()
|
||||
let typingGroups: TypingGroup[] = []
|
||||
|
||||
$: objectId = peerId ? `peer${peerId}` : cardId
|
||||
|
||||
$: void updateTypingPersons(typingInfo)
|
||||
|
||||
async function updateTypingPersons (typingInfo: Map<string, TypingInfo>): Promise<void> {
|
||||
@@ -60,21 +62,22 @@
|
||||
const groups: TypingGroup[] = []
|
||||
|
||||
for (const [status, personIds] of groupedByStatus.entries()) {
|
||||
const personRefs = await getPersonRefsBySocialIds(client, personIds)
|
||||
const persons = await getPersonsByPersonRefs(Object.values(personRefs))
|
||||
const persons = await getPersonsByPersonIds(personIds)
|
||||
const names = Array.from(persons.values())
|
||||
.map((person) => getName(hierarchy, person))
|
||||
.sort((name1, name2) => name1.localeCompare(name2))
|
||||
|
||||
const displayNames = names.slice(0, maxTypingPersons).join(', ')
|
||||
const moreCount = Math.max(names.length - maxTypingPersons, 0)
|
||||
if (names.length > 0) {
|
||||
const displayNames = names.slice(0, maxTypingPersons).join(', ')
|
||||
const moreCount = Math.max(names.length - maxTypingPersons, 0)
|
||||
|
||||
groups.push({
|
||||
status,
|
||||
names: displayNames,
|
||||
count: names.length,
|
||||
moreCount
|
||||
})
|
||||
groups.push({
|
||||
status,
|
||||
names: displayNames,
|
||||
count: names.length,
|
||||
moreCount
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
groups.sort((a, b) => a.status.localeCompare(b.status))
|
||||
@@ -91,7 +94,7 @@
|
||||
class="root h-4 mt-1 mb-1 ml-0-5 overflow-label"
|
||||
use:typing={{
|
||||
socialId: acc.primarySocialId,
|
||||
objectId: cardId,
|
||||
objectId,
|
||||
onTyping: handleTyping
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -461,7 +461,7 @@
|
||||
if (message !== undefined) return
|
||||
if (!isEmptyMarkup(draft.content)) {
|
||||
throttle.call(() => {
|
||||
void setTyping(acc.primarySocialId, card._id)
|
||||
void setTyping(acc.primarySocialId, card.peerId ? `peer:${card.peerId}` : card._id)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -705,7 +705,7 @@
|
||||
</div>
|
||||
|
||||
{#if message === undefined}
|
||||
<TypingPresenter cardId={card._id} />
|
||||
<TypingPresenter cardId={card._id} peerId={card.peerId} />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
<!--
|
||||
// Copyright © 2025 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 { ActivityCollaborativeChange } from '@hcengineering/communication-types'
|
||||
import { MarkupDiffPresenter } from '@hcengineering/view-resources'
|
||||
import activity from '@hcengineering/activity'
|
||||
import ui, { Label } from '@hcengineering/ui'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import communication from '../../../plugin'
|
||||
|
||||
export let model: AttributeModel | undefined = undefined
|
||||
export let update: ActivityCollaborativeChange
|
||||
|
||||
$: isTooLarge = update.value === activity.string.ValueTooLarge || update.prevValue === activity.string.ValueTooLarge
|
||||
|
||||
let isDiffShown = false
|
||||
|
||||
function toggleShowMore (): void {
|
||||
isDiffShown = !isDiffShown
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if model !== undefined}
|
||||
<Label label={model.label} />
|
||||
<span class="lower"><Label label={activity.string.Edited} /></span>
|
||||
{/if}
|
||||
{#if isTooLarge}
|
||||
<div class="unset row overflow-label">
|
||||
<Label label={activity.string.ValueTooLarge} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="showMore" on:click={toggleShowMore}>
|
||||
<div class="triangle" class:left={!isDiffShown} class:down={isDiffShown} />
|
||||
<Label label={isDiffShown ? ui.string.ShowLess : ui.string.ShowMore} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isDiffShown}
|
||||
<MarkupDiffPresenter value={update.value} prevValue={update.prevValue} />
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.showMore {
|
||||
color: var(--global-primary-LinkColor);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
gap: 0.5rem;
|
||||
|
||||
.triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&.left {
|
||||
border-top: 0.25rem solid transparent;
|
||||
border-bottom: 0.25rem solid transparent;
|
||||
border-left: 0.25rem solid var(--global-primary-LinkColor);
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.down {
|
||||
border-left: 0.25rem solid transparent;
|
||||
border-right: 0.25rem solid transparent;
|
||||
border-top: 0.25rem solid var(--global-primary-LinkColor);
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--global-focus-BorderColor);
|
||||
|
||||
.triangle {
|
||||
&.left {
|
||||
border-left-color: var(--global-focus-BorderColor);
|
||||
}
|
||||
|
||||
&.down {
|
||||
border-top-color: var(--global-focus-BorderColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+3
@@ -23,6 +23,7 @@
|
||||
import { Card } from '@hcengineering/card'
|
||||
import ActivityUpdateTypeViewer from './ActivityUpdateTypeViewer.svelte'
|
||||
import ActivityUpdateProcessViewer from './ActivityUpdateProcessViewer.svelte'
|
||||
import ActivityCollaborativeContentViewer from './ActivityCollaborativeContentViewer.svelte'
|
||||
|
||||
export let model: AttributeModel | undefined = undefined
|
||||
export let update: ActivityUpdate
|
||||
@@ -41,4 +42,6 @@
|
||||
<ActivityUpdateTypeViewer {update} />
|
||||
{:else if update.type === ActivityUpdateType.Process}
|
||||
<ActivityUpdateProcessViewer {update} {content} />
|
||||
{:else if update.type === ActivityUpdateType.CollaborativeChange}
|
||||
<ActivityCollaborativeContentViewer {model} {update} />
|
||||
{/if}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.7.5",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/core": "^0.7.10",
|
||||
"@hcengineering/contact": "^0.7.0",
|
||||
"@hcengineering/ui": "^0.7.0",
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
"@hcengineering/card": "^0.7.0",
|
||||
"@hcengineering/communication": "^0.7.0",
|
||||
"@hcengineering/communication-resources": "^0.7.0",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-shared": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/communication-shared": "^0.7.8",
|
||||
"@hcengineering/contact": "^0.7.0",
|
||||
"@hcengineering/contact-resources": "^0.7.0",
|
||||
"@hcengineering/core": "^0.7.10",
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<div class="labels">
|
||||
{#if client.getHierarchy().isDerived(navItem._class, cardPlugin.class.Card)}
|
||||
{@const label = client.getHierarchy().getClass(doc?._class ?? navItem._class).label}
|
||||
<span class="title--bold overflow-label clear-mins" use:tooltip={{ label }}>
|
||||
<span class="title--bold overflow-label clear-mins">
|
||||
<Label {label} />
|
||||
</span>
|
||||
{#if doc}
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
import PresenceContext from './PresenceContext.svelte'
|
||||
|
||||
export let object: Doc
|
||||
export let presenceId: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#key object._id}
|
||||
<PresenceContext {object} />
|
||||
<PresenceContext {object} {presenceId} />
|
||||
{/key}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import { followee, toggleFollowee } from '../store'
|
||||
|
||||
export let object: Doc
|
||||
export let presenceId: string | undefined = undefined
|
||||
export let size: IconSize = 'small'
|
||||
export let limit: number = 4
|
||||
|
||||
@@ -55,7 +56,7 @@
|
||||
<div
|
||||
use:presence={{
|
||||
personId: me,
|
||||
objectId: object._id,
|
||||
objectId: presenceId ?? object._id,
|
||||
objectClass: object._class,
|
||||
onPresence
|
||||
}}
|
||||
|
||||
@@ -21,18 +21,19 @@
|
||||
import { updatePresence, deletePresence } from '../presence'
|
||||
|
||||
export let object: Doc
|
||||
export let presenceId: string | undefined = undefined
|
||||
export let presenceTtlSeconds: number = 5
|
||||
export let presenceUpdateSeconds: number = 2
|
||||
|
||||
const personId = getCurrentEmployee()
|
||||
|
||||
async function doUpdatePresence (): Promise<void> {
|
||||
const presence = { personId, objectId: object._id, objectClass: object._class }
|
||||
const presence = { personId, objectId: presenceId ?? object._id, objectClass: object._class }
|
||||
await updatePresence(presence, presenceTtlSeconds)
|
||||
}
|
||||
|
||||
async function doDeletePresence (object: Doc): Promise<void> {
|
||||
const presence = { personId, objectId: object._id, objectClass: object._class }
|
||||
async function doDeletePresence (object: Doc, presenceId?: string): Promise<void> {
|
||||
const presence = { personId, objectId: presenceId ?? object._id, objectClass: object._class }
|
||||
await deletePresence(presence)
|
||||
}
|
||||
|
||||
@@ -41,15 +42,20 @@
|
||||
const interval = setInterval(doUpdatePresence, presenceUpdateSeconds * 1000)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
void doDeletePresence(object)
|
||||
void doDeletePresence(object, presenceId)
|
||||
}
|
||||
})
|
||||
|
||||
let previousObject: Doc = object
|
||||
let prevPresenceId: string | undefined = presenceId
|
||||
|
||||
$: if (object !== undefined && (object._id !== previousObject._id || object._class !== previousObject._class)) {
|
||||
$: if (
|
||||
object !== undefined &&
|
||||
(object._id !== previousObject._id || object._class !== previousObject._class || presenceId !== prevPresenceId)
|
||||
) {
|
||||
void doDeletePresence(previousObject)
|
||||
previousObject = object
|
||||
prevPresenceId = presenceId
|
||||
void doUpdatePresence()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -19,13 +19,13 @@ import presentation, { createPulseClient } from '@hcengineering/presentation'
|
||||
|
||||
export interface PresenceInfo {
|
||||
personId: Ref<Person>
|
||||
objectId: Ref<Doc>
|
||||
objectId: string
|
||||
objectClass: Ref<Class<Doc>>
|
||||
}
|
||||
|
||||
export interface PresenceActionParams {
|
||||
personId: Ref<Employee>
|
||||
objectId: Ref<Doc>
|
||||
objectId: string
|
||||
objectClass: Ref<Class<Doc>>
|
||||
onPresence: (presence: Map<string, Ref<Person>>) => void
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export function presence (node: HTMLElement, params: PresenceActionParams): any
|
||||
|
||||
export async function subscribePresence (
|
||||
objectClass: Ref<Class<Doc>>,
|
||||
objectId: Ref<Doc>,
|
||||
objectId: string,
|
||||
callback: Callback<PresenceInfo | undefined>
|
||||
): Promise<UnsubscribeCallback> {
|
||||
const client = await createPulseClient()
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import { type UnsubscribeCallback, type Callback } from '@hcengineering/hulypulse-client'
|
||||
import { type IntlString, getMetadata } from '@hcengineering/platform'
|
||||
import presentation, { createPulseClient } from '@hcengineering/presentation'
|
||||
import { type Doc, type Ref, type PersonId } from '@hcengineering/core'
|
||||
import { type PersonId } from '@hcengineering/core'
|
||||
|
||||
const typingDelaySeconds = 2
|
||||
|
||||
@@ -24,13 +24,13 @@ function getWorkspace (): string {
|
||||
|
||||
export interface TypingInfo {
|
||||
socialId: PersonId
|
||||
objectId: Ref<Doc>
|
||||
objectId: string
|
||||
status?: IntlString
|
||||
}
|
||||
|
||||
export interface TypingActionParams {
|
||||
socialId: PersonId
|
||||
objectId: Ref<Doc>
|
||||
objectId: string
|
||||
onTyping: (presence: Map<string, TypingInfo>) => void
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function typing (node: HTMLElement, params: TypingActionParams): any {
|
||||
}
|
||||
|
||||
export async function subscribeTyping (
|
||||
objectId: Ref<Doc>,
|
||||
objectId: string,
|
||||
callback: Callback<TypingInfo | undefined>
|
||||
): Promise<UnsubscribeCallback> {
|
||||
const client = await createPulseClient()
|
||||
@@ -100,7 +100,7 @@ export async function subscribeTyping (
|
||||
return async () => false
|
||||
}
|
||||
|
||||
export async function setTyping (socialId: PersonId, objectId: Ref<Doc>, status?: IntlString): Promise<void> {
|
||||
export async function setTyping (socialId: PersonId, objectId: string, status?: IntlString): Promise<void> {
|
||||
const client = await createPulseClient()
|
||||
|
||||
if (client !== undefined) {
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
}
|
||||
|
||||
function getKeys (_class: Ref<Class<MasterTag>>): AnyAttribute[] {
|
||||
const ignoreKeys = ['_class', 'content', 'parent', 'attachments', 'todos']
|
||||
const ignoreKeys = ['_class', 'parent', 'attachments', 'todos']
|
||||
const attributes = hierarchy.getAllAttributes(_class, core.class.Doc)
|
||||
const res: AnyAttribute[] = []
|
||||
for (const [key, attr] of attributes) {
|
||||
|
||||
@@ -278,4 +278,13 @@
|
||||
<symbol id="print" viewBox="0 0 32 32">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C7.89543 4 7 4.89543 7 6V26C7 27.1046 7.89543 28 9 28H23C24.1046 28 25 27.1046 25 26V12H21C18.7909 12 17 10.2091 17 8V4H9ZM19 4.41421V8C19 9.10457 19.8954 10 21 10H24.5858L19 4.41421ZM5 6C5 3.79086 6.79086 2 9 2H18.5858C19.1162 2 19.6249 2.21071 20 2.58579L26.4142 9C26.7893 9.37507 27 9.88378 27 10.4142V26C27 28.2091 25.2091 30 23 30H9C6.79086 30 5 28.2091 5 26V6ZM10 17C10 16.4477 10.4477 16 11 16H21C21.5523 16 22 16.4477 22 17C22 17.5523 21.5523 18 21 18H11C10.4477 18 10 17.5523 10 17ZM10 23C10 22.4477 10.4477 22 11 22H21C21.5523 22 22 22.4477 22 23C22 23.5523 21.5523 24 21 24H11C10.4477 24 10 23.5523 10 23Z" />
|
||||
</symbol>
|
||||
</svg>
|
||||
<symbol id="ai-star" viewBox="0 0 24 24">
|
||||
<path d="M19 22C19 22.5523 18.5523 23 18 23C17.4477 23 17 22.5523 17 22C17 21.4477 17.4477 21 18 21C18.5523 21 19 21.4477 19 22Z" fill="currentColor"/>
|
||||
<path d="M23 18C23 18.5523 22.5523 19 22 19C21.4477 19 21 18.5523 21 18C21 17.4477 21.4477 17 22 17C22.5523 17 23 17.4477 23 18Z" fill="currentColor"/>
|
||||
<path d="M3 6C3 6.55228 2.55228 7 2 7C1.44772 7 1 6.55228 1 6C1 5.44772 1.44772 5 2 5C2.55228 5 3 5.44772 3 6Z" fill="currentColor"/>
|
||||
<path d="M7 2C7 2.55228 6.55228 3 6 3C5.44772 3 5 2.55228 5 2C5 1.44772 5.44772 1 6 1C6.55228 1 7 1.44772 7 2Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 6.48292L11.3478 9.17257C11.0871 10.2477 10.2477 11.0871 9.17257 11.3478L6.48292 12L9.17257 12.6522C10.2477 12.9129 11.0871 13.7523 11.3478 14.8274L12 17.5171L12.6522 14.8274C12.9129 13.7523 13.7523 12.9129 14.8274 12.6522L17.5171 12L14.8274 11.3478C13.7523 11.0871 12.9129 10.2477 12.6522 9.17257L12 6.48292ZM13.3052 5.02656C12.9733 3.65781 11.0267 3.65781 10.6948 5.02656L9.78151 8.79277C9.66301 9.28146 9.28146 9.66301 8.79277 9.78151L5.02656 10.6948C3.65781 11.0267 3.65781 12.9733 5.02656 13.3052L8.79277 14.2185C9.28146 14.337 9.66301 14.7185 9.78151 15.2072L10.6948 18.9734C11.0267 20.3422 12.9733 20.3422 13.3052 18.9734L14.2185 15.2072C14.337 14.7185 14.7185 14.337 15.2072 14.2185L18.9734 13.3052C20.3422 12.9733 20.3422 11.0267 18.9734 10.6948L15.2072 9.78151C14.7185 9.66301 14.337 9.28146 14.2185 8.79277L13.3052 5.02656Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.00462 22.3147C8.83081 22.7804 8.31241 23.017 7.84673 22.8432C4.8303 21.7173 2.40732 19.3801 1.16973 16.4228C0.977847 15.9643 1.194 15.4371 1.65252 15.2452C2.11105 15.0533 2.63831 15.2694 2.8302 15.728C3.8762 18.2274 5.92654 20.2052 8.47615 21.1568C8.94183 21.3306 9.17843 21.849 9.00462 22.3147Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22.3153 9.02077C21.8498 9.19493 21.3312 8.95873 21.157 8.49319C20.1979 5.92954 18.1971 3.87072 15.6693 2.83251C15.2095 2.64366 14.9899 2.11784 15.1787 1.65806C15.3676 1.19827 15.8934 0.978632 16.3532 1.16748C19.3427 2.39533 21.7076 4.82798 22.8429 7.86246C23.0171 8.32801 22.7809 8.8466 22.3153 9.02077Z" fill="currentColor"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
@@ -68,7 +68,8 @@ loadMetadata(view.icon, {
|
||||
MasterDetail: `${icons}#master-detail`,
|
||||
Tree: `${icons}#tree`,
|
||||
Document: `${icons}#document`,
|
||||
Print: `${icons}#print`
|
||||
Print: `${icons}#print`,
|
||||
AiStar: `${icons}#ai-star`
|
||||
})
|
||||
loadMetadata(core.icon, {
|
||||
TypeString: `${icons}#string`,
|
||||
|
||||
@@ -28,6 +28,6 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="overflow-label" use:tooltip={tooltipParams}>
|
||||
<span class="overflow-label px-3" use:tooltip={tooltipParams}>
|
||||
{value ?? ''}
|
||||
</span>
|
||||
|
||||
@@ -304,7 +304,8 @@ const view = plugin(viewId, {
|
||||
MasterDetail: '' as Asset,
|
||||
Tree: '' as Asset,
|
||||
Document: '' as Asset,
|
||||
Print: '' as Asset
|
||||
Print: '' as Asset,
|
||||
AiStar: '' as Asset
|
||||
},
|
||||
category: {
|
||||
General: '' as Ref<ActionCategory>,
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"@hcengineering/support-resources": "^0.7.0",
|
||||
"@hcengineering/view-resources": "^0.7.0",
|
||||
"@hcengineering/inbox": "^0.7.0",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"fast-copy": "^3.0.2",
|
||||
"@hcengineering/analytics": "^0.7.5",
|
||||
"@hcengineering/account-client": "^0.7.7",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
stream hardcoreeng/service_stream:0.5.7
|
||||
stream hardcoreeng/service_stream:0.5.8
|
||||
|
||||
@@ -76,8 +76,8 @@
|
||||
"@hcengineering/postgres": "^0.7.9",
|
||||
"@hcengineering/mongo": "^0.7.8",
|
||||
"@hcengineering/kafka": "^0.7.8",
|
||||
"@hcengineering/communication-server": "^0.7.7",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.7",
|
||||
"@hcengineering/communication-server": "^0.7.9",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.9",
|
||||
"@hcengineering/hulylake-client": "^0.7.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
"@hcengineering/server-token": "^0.7.5",
|
||||
"@hcengineering/attachment": "^0.7.0",
|
||||
"@hcengineering/drive": "^0.7.0",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.9",
|
||||
"dotenv": "~16.0.0",
|
||||
"kafkajs": "^2.2.4"
|
||||
}
|
||||
|
||||
@@ -61,8 +61,8 @@
|
||||
"dependencies": {
|
||||
"@hcengineering/analytics": "^0.7.5",
|
||||
"@hcengineering/analytics-service": "^0.7.5",
|
||||
"@hcengineering/communication-server": "^0.7.7",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.7",
|
||||
"@hcengineering/communication-server": "^0.7.9",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.9",
|
||||
"@hcengineering/contact": "^0.7.0",
|
||||
"@hcengineering/core": "^0.7.10",
|
||||
"@hcengineering/kafka": "^0.7.8",
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
"@hcengineering/server-core": "^0.7.8",
|
||||
"@hcengineering/server-notification-resources": "^0.7.0",
|
||||
"@hcengineering/text-core": "^0.7.5",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.9",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/server-card": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
"@hcengineering/core": "^0.7.10",
|
||||
"@hcengineering/platform": "^0.7.5",
|
||||
"@hcengineering/server-core": "^0.7.8",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.9",
|
||||
"@hcengineering/communication": "^0.7.0",
|
||||
"@hcengineering/server-contact": "^0.7.0",
|
||||
"@hcengineering/contact": "^0.7.0"
|
||||
|
||||
@@ -541,21 +541,29 @@ async function updateParentInfoName (
|
||||
async function OnThreadCreate (ctx: TxCreateDoc<Card>[], control: TriggerControl): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
for (const tx of ctx) {
|
||||
if (tx.space === core.space.DerivedTx) continue
|
||||
const doc = TxProcessor.createDoc2Doc(tx)
|
||||
if (doc.peerId != null) continue
|
||||
const parent = doc.parentInfo?.[0]
|
||||
if (parent == null) continue
|
||||
if (!control.hierarchy.isDerived(parent._class, communication.type.Direct)) continue
|
||||
const direct = (await control.findAll(control.ctx, parent._class, { _id: parent._id }, { limit: 1 }))[0] as Direct
|
||||
if (direct == null) continue
|
||||
|
||||
res.push(...(await createThreadCardPeers(direct, doc, control)))
|
||||
const peerId = generateId()
|
||||
|
||||
res.push(control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { peerId }))
|
||||
res.push(...(await createThreadCardPeers(direct, doc, control, peerId)))
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function createThreadCardPeers (direct: Direct, doc: Card, control: TriggerControl): Promise<Tx[]> {
|
||||
async function createThreadCardPeers (
|
||||
direct: Direct,
|
||||
doc: Card,
|
||||
control: TriggerControl,
|
||||
peerId: string
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
const cardIds = new Map<Ref<Card>, Ref<Space>>([[doc._id, doc.space]])
|
||||
const members = direct.members ?? []
|
||||
@@ -600,7 +608,8 @@ async function createThreadCardPeers (direct: Direct, doc: Card, control: Trigge
|
||||
_class,
|
||||
personSpace._id,
|
||||
{
|
||||
...doc
|
||||
...doc,
|
||||
peerId
|
||||
},
|
||||
_id
|
||||
)
|
||||
@@ -627,18 +636,20 @@ async function createThreadCardPeers (direct: Direct, doc: Card, control: Trigge
|
||||
}
|
||||
|
||||
if (cardIds.size > 1) {
|
||||
const group = generateId()
|
||||
let newValue = true
|
||||
for (const [cardId, spaceId] of cardIds.entries()) {
|
||||
const event: CreatePeerEvent = {
|
||||
type: PeerEventType.CreatePeer,
|
||||
workspaceId: control.workspace.uuid, // TODO: person_workspace
|
||||
cardId,
|
||||
kind: 'card',
|
||||
value: group,
|
||||
value: peerId,
|
||||
extra: { space: spaceId },
|
||||
date: new Date(doc.modifiedOn)
|
||||
date: new Date(doc.modifiedOn),
|
||||
options: { newValue }
|
||||
}
|
||||
await control.domainRequest(control.ctx, 'communication' as OperationDomain, { event })
|
||||
newValue = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -656,7 +667,12 @@ function getDirectTitle (employees: Employee[], me: Ref<Person>): string {
|
||||
}
|
||||
}
|
||||
|
||||
async function createDirectCardPeers (doc: Card, members: Ref<Person>[], control: TriggerControl): Promise<Tx[]> {
|
||||
async function createDirectCardPeers (
|
||||
doc: Card,
|
||||
members: Ref<Person>[],
|
||||
control: TriggerControl,
|
||||
peerId: string
|
||||
): Promise<Tx[]> {
|
||||
const res: Tx[] = []
|
||||
const cardIds = new Map<Ref<Card>, Ref<Space>>([[doc._id, doc.space]])
|
||||
if (members.length === 0) return []
|
||||
@@ -683,6 +699,7 @@ async function createDirectCardPeers (doc: Card, members: Ref<Person>[], control
|
||||
personSpace._id,
|
||||
{
|
||||
...doc,
|
||||
peerId,
|
||||
title
|
||||
},
|
||||
_id
|
||||
@@ -702,18 +719,20 @@ async function createDirectCardPeers (doc: Card, members: Ref<Person>[], control
|
||||
}
|
||||
|
||||
if (cardIds.size > 1) {
|
||||
const group = generateId()
|
||||
let newValue = true
|
||||
for (const [cardId, spaceId] of cardIds.entries()) {
|
||||
const event: CreatePeerEvent = {
|
||||
type: PeerEventType.CreatePeer,
|
||||
workspaceId: control.workspace.uuid, // TODO: person_workspace
|
||||
cardId,
|
||||
kind: 'card',
|
||||
value: group,
|
||||
value: peerId,
|
||||
extra: { space: spaceId },
|
||||
date: new Date(doc.modifiedOn)
|
||||
date: new Date(doc.modifiedOn),
|
||||
options: { newValue }
|
||||
}
|
||||
await control.domainRequest(control.ctx, 'communication' as OperationDomain, { event })
|
||||
newValue = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -724,11 +743,13 @@ async function OnDirectCreate (ctx: TxCreateDoc<Direct>[], control: TriggerContr
|
||||
const res: Tx[] = []
|
||||
|
||||
for (const tx of ctx) {
|
||||
if (tx.space === core.space.DerivedTx) continue
|
||||
const doc = TxProcessor.createDoc2Doc(tx)
|
||||
if (doc.peerId != null) continue
|
||||
const members = doc.members ?? []
|
||||
const peerId = generateId()
|
||||
|
||||
res.push(...(await createDirectCardPeers(doc, members, control)))
|
||||
res.push(control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { peerId }))
|
||||
res.push(...(await createDirectCardPeers(doc, members, control, peerId)))
|
||||
}
|
||||
|
||||
return res
|
||||
|
||||
@@ -17,11 +17,13 @@ import cardPlugin, { Card, MasterTag, Tag } from '@hcengineering/card'
|
||||
import core, {
|
||||
Association,
|
||||
checkMixinKey,
|
||||
Class,
|
||||
Data,
|
||||
Doc,
|
||||
findProperty,
|
||||
generateId,
|
||||
getObjectValue,
|
||||
makeDocCollabId,
|
||||
matchQuery,
|
||||
Ref,
|
||||
Relation,
|
||||
@@ -428,12 +430,41 @@ export async function CreateToDo (
|
||||
}
|
||||
}
|
||||
|
||||
async function getContent (
|
||||
control: ProcessControl,
|
||||
source: string,
|
||||
_id: Ref<Card>,
|
||||
_class: Ref<Class<Card>>
|
||||
): Promise<string> {
|
||||
const collabClient = control.collaboratorFactory()
|
||||
const data = source.split('-')
|
||||
const sourceId = data[0]
|
||||
const sourceAttr = data[1]
|
||||
if (isEmpty(sourceId) || isEmpty(sourceAttr)) {
|
||||
throw processError(process.error.RequiredParamsNotProvided, { params: 'content' })
|
||||
}
|
||||
const sourceCard = await control.client.findOne(cardPlugin.class.Card, { _id: sourceId as Ref<Card> })
|
||||
if (sourceCard === undefined) {
|
||||
throw processError(process.error.ObjectNotFound, { _id: sourceId })
|
||||
}
|
||||
const markup = await collabClient.getMarkup(makeDocCollabId(sourceCard, sourceAttr))
|
||||
const ref = await collabClient.createMarkup(
|
||||
{
|
||||
objectClass: _class,
|
||||
objectId: _id,
|
||||
objectAttr: 'content'
|
||||
},
|
||||
markup
|
||||
)
|
||||
return ref
|
||||
}
|
||||
|
||||
export async function CreateCard (
|
||||
params: MethodParams<Card>,
|
||||
execution: Execution,
|
||||
control: ProcessControl
|
||||
): Promise<ExecuteResult> {
|
||||
const { _class, title, ...attrs } = params
|
||||
const { _class, title, content, ...attrs } = params
|
||||
for (const key in { _class, title }) {
|
||||
const val = (params as any)[key]
|
||||
if (isEmpty(val)) {
|
||||
@@ -441,10 +472,15 @@ export async function CreateCard (
|
||||
}
|
||||
}
|
||||
const _id = generateId<Card>()
|
||||
const newContent =
|
||||
content !== undefined ? await getContent(control, content, _id, _class as Ref<Class<Card>>) : undefined
|
||||
const data = {
|
||||
title,
|
||||
...attrs
|
||||
} as any
|
||||
if (newContent !== undefined) {
|
||||
data.content = content
|
||||
}
|
||||
const tx = control.client.txFactory.createTxCreateDoc(_class as Ref<MasterTag>, execution.space, data, _id)
|
||||
const res: Tx[] = [tx]
|
||||
const rollback: Tx[] = [control.client.txFactory.createTxRemoveDoc(_class as Ref<MasterTag>, execution.space, _id)]
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"@hcengineering/card": "^0.7.0",
|
||||
"@hcengineering/process": "^0.7.0",
|
||||
"@hcengineering/platform": "^0.7.5",
|
||||
"@hcengineering/collaborator-client": "^0.7.5",
|
||||
"@hcengineering/server-core": "^0.7.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Card } from '@hcengineering/card'
|
||||
import { Doc, MeasureContext, PersonId, Ref, Timestamp, Tx, TxOperations, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { Execution, ExecutionError, MethodParams, Trigger, UserResult } from '@hcengineering/process'
|
||||
import { CollaboratorClient } from '@hcengineering/collaborator-client'
|
||||
|
||||
export type ExecuteFunc = (
|
||||
params: MethodParams<Doc>,
|
||||
@@ -41,6 +42,7 @@ export interface ProcessMessage {
|
||||
export interface ProcessControl {
|
||||
ctx: MeasureContext
|
||||
client: TxOperations
|
||||
collaboratorFactory: () => CollaboratorClient
|
||||
cache: Map<string, any>
|
||||
messageContext: Record<string, any>
|
||||
workspace: WorkspaceUuid
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
"@types/jest": "^29.5.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.9",
|
||||
"@hcengineering/activity": "^0.7.0",
|
||||
"@hcengineering/analytics": "^0.7.5",
|
||||
"@hcengineering/core": "^0.7.10",
|
||||
|
||||
@@ -17,7 +17,9 @@ import activity, { DocUpdateMessage } from '@hcengineering/activity'
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { loadCollabJson, loadCollabYdoc, saveCollabJson, saveCollabYdoc } from '@hcengineering/collaboration'
|
||||
import { decodeDocumentId } from '@hcengineering/collaborator-client'
|
||||
import core, { AttachedData, MeasureContext, Ref, Space, TxOperations } from '@hcengineering/core'
|
||||
import { CreateMessageEvent, MessageEventType } from '@hcengineering/communication-sdk-types'
|
||||
import { ActivityCollaborativeChange, ActivityUpdateType, MessageType } from '@hcengineering/communication-types'
|
||||
import core, { AttachedData, Doc, MeasureContext, OperationDomain, Ref, Space, TxOperations } from '@hcengineering/core'
|
||||
import { StorageAdapter } from '@hcengineering/server-core'
|
||||
import { areEqualMarkups } from '@hcengineering/text'
|
||||
import { markupToYDoc } from '@hcengineering/text-ydoc'
|
||||
@@ -263,11 +265,11 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
|
||||
await ctx.with(
|
||||
'activity',
|
||||
{},
|
||||
() => {
|
||||
async () => {
|
||||
const space = hierarchy.isDerived(current._class, core.class.Space)
|
||||
? (current._id as Ref<Space>)
|
||||
: current.space
|
||||
|
||||
await sendEvent(client, objectAttr, prevValue, currValue, current)
|
||||
const data: AttachedData<DocUpdateMessage> = {
|
||||
objectId,
|
||||
objectClass,
|
||||
@@ -282,7 +284,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
|
||||
isMixin: hierarchy.isMixin(objectClass)
|
||||
}
|
||||
}
|
||||
return client.addCollection(
|
||||
return await client.addCollection(
|
||||
activity.class.DocUpdateMessage,
|
||||
space,
|
||||
current._id,
|
||||
@@ -301,6 +303,36 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEvent (
|
||||
client: Omit<TxOperations, 'close'>,
|
||||
attrKey: string,
|
||||
prevValue: string,
|
||||
value: string,
|
||||
doc: Doc
|
||||
): Promise<void> {
|
||||
const eventData: ActivityCollaborativeChange = {
|
||||
type: ActivityUpdateType.CollaborativeChange,
|
||||
attrKey,
|
||||
value,
|
||||
prevValue
|
||||
}
|
||||
const event: CreateMessageEvent = {
|
||||
type: MessageEventType.CreateMessage,
|
||||
messageType: MessageType.Activity,
|
||||
cardId: doc._id,
|
||||
cardType: doc._class,
|
||||
extra: {
|
||||
action: 'update',
|
||||
update: eventData
|
||||
},
|
||||
content: '',
|
||||
socialId: client.txFactory.account,
|
||||
date: new Date()
|
||||
}
|
||||
|
||||
await client.domainRequest('communication' as OperationDomain, { event })
|
||||
}
|
||||
|
||||
async function withRetry<T> (
|
||||
ctx: MeasureContext,
|
||||
retries: number,
|
||||
|
||||
@@ -49,10 +49,10 @@
|
||||
"@hcengineering/drive": "^0.7.0",
|
||||
"fast-equals": "^5.2.2",
|
||||
"@hcengineering/storage": "^0.7.5",
|
||||
"@hcengineering/communication-rest-client": "^0.7.7",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.7",
|
||||
"@hcengineering/communication-shared": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-rest-client": "^0.7.9",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.9",
|
||||
"@hcengineering/communication-shared": "^0.7.8",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/hulylake-client": "^0.7.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,8 +180,8 @@
|
||||
"@hcengineering/card": "^0.7.0",
|
||||
"@hcengineering/mail": "^0.7.0",
|
||||
"@hcengineering/kafka": "^0.7.8",
|
||||
"@hcengineering/communication-types": "^0.7.7",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.7",
|
||||
"@hcengineering/communication-types": "^0.7.9",
|
||||
"@hcengineering/communication-sdk-types": "^0.7.9",
|
||||
"@hcengineering/communication": "^0.7.0",
|
||||
"@hcengineering/communication-assets": "^0.7.0"
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deepgram/sdk": "^3.12.1",
|
||||
"@deepgram/sdk": "^4.11.2",
|
||||
"@livekit/agents": "^0.7.4",
|
||||
"@livekit/rtc-node": "^0.13.11",
|
||||
"dotenv": "^16.4.5",
|
||||
|
||||
+5
-5
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@deepgram/sdk':
|
||||
specifier: ^3.12.1
|
||||
version: 3.12.1
|
||||
specifier: ^4.11.2
|
||||
version: 4.11.2
|
||||
'@livekit/agents':
|
||||
specifier: ^0.7.4
|
||||
version: 0.7.4(@livekit/rtc-node@0.13.11)
|
||||
@@ -70,8 +70,8 @@ packages:
|
||||
resolution: {integrity: sha512-8B1C/oTxTxyHlSFubAhNRgCbQ2SQ5wwvtlByn8sDYZvdDtdn/VE2yEPZ4BvUnrKWmsbTQY6/ooLV+9Ka2qmDSQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@deepgram/sdk@3.12.1':
|
||||
resolution: {integrity: sha512-MNnCnlyxdf0IY4Dkt+YcgCb8sy14zAzJ5BCNMwzv20tSxweoj8mk6eM063zQTuHWc3x2giKvS9x6/IBtV9zJLg==}
|
||||
'@deepgram/sdk@4.11.2':
|
||||
resolution: {integrity: sha512-lKGxuXxlSixC8bB0BnzmIpbVjUSgYtz17cqvrgv0ZjmazgUPkuUj9egQPj6k+fbPX8wRzWEqlhrL/DXlXqeDXA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@esbuild/aix-ppc64@0.20.2':
|
||||
@@ -1531,7 +1531,7 @@ snapshots:
|
||||
dependencies:
|
||||
dayjs: 1.11.13
|
||||
|
||||
'@deepgram/sdk@3.12.1':
|
||||
'@deepgram/sdk@4.11.2':
|
||||
dependencies:
|
||||
'@deepgram/captions': 1.2.0
|
||||
'@types/node': 18.19.68
|
||||
|
||||
@@ -101,7 +101,15 @@ export default defineAgent({
|
||||
return
|
||||
}
|
||||
|
||||
const stt = getStt(ctx.room)
|
||||
const workspace = (roomName.split('_')[0] ?? '').trim()
|
||||
|
||||
if (workspace === '') {
|
||||
console.error('Workspace is not defined', roomName)
|
||||
ctx.shutdown()
|
||||
return
|
||||
}
|
||||
|
||||
const stt = getStt(ctx.room, workspace)
|
||||
|
||||
if (stt === undefined) {
|
||||
console.error('Transcription provider is not configured')
|
||||
|
||||
@@ -35,7 +35,7 @@ interface Config {
|
||||
DgVadEvents: boolean
|
||||
DgPunctuate: boolean
|
||||
DgSmartFormat: boolean
|
||||
DgNoDelay: boolean,
|
||||
DgNoDelay: boolean
|
||||
DgSampleRate: number
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,10 @@ export class STT implements Stt {
|
||||
|
||||
private transcriptionCount = 0
|
||||
|
||||
constructor (readonly room: Room) {
|
||||
constructor (
|
||||
readonly room: Room,
|
||||
readonly workspace: string
|
||||
) {
|
||||
this.deepgram = createClient(config.DeepgramApiKey)
|
||||
}
|
||||
|
||||
@@ -154,6 +157,7 @@ export class STT implements Stt {
|
||||
|
||||
return {
|
||||
...options,
|
||||
extra: `workspace:${this.workspace}`,
|
||||
encoding: 'linear16',
|
||||
channels: stream.numChannels,
|
||||
sample_rate: stream.sampleRate,
|
||||
|
||||
@@ -5,12 +5,12 @@ import * as openai from './openai/stt.js'
|
||||
import config from './config.js'
|
||||
import { Stt } from './type.js'
|
||||
|
||||
export function getStt (room: Room): Stt | undefined {
|
||||
export function getStt (room: Room, worksapce: string): Stt | undefined {
|
||||
const provider = config.SttProvider
|
||||
|
||||
switch (provider) {
|
||||
case 'deepgram':
|
||||
return new dg.STT(room)
|
||||
return new dg.STT(room, worksapce)
|
||||
case 'openai':
|
||||
return new openai.STT(room)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,9 @@
|
||||
"@hcengineering/account": "^0.7.0",
|
||||
"@hcengineering/account-client": "^0.7.7",
|
||||
"@hcengineering/ai-bot": "^0.7.0",
|
||||
"@hcengineering/analytics-service": "^0.7.5",
|
||||
"@hcengineering/attachment": "^0.7.0",
|
||||
"@hcengineering/billing-client": "^0.7.0",
|
||||
"@hcengineering/chunter": "^0.7.0",
|
||||
"@hcengineering/client": "^0.7.6",
|
||||
"@hcengineering/client-resources": "^0.7.6",
|
||||
@@ -70,6 +72,7 @@
|
||||
"@hcengineering/openai": "^0.7.0",
|
||||
"@hcengineering/platform": "^0.7.5",
|
||||
"@hcengineering/rank": "^0.7.5",
|
||||
"@hcengineering/retry": "^0.7.5",
|
||||
"@hcengineering/server-ai-bot": "^0.7.0",
|
||||
"@hcengineering/server-client": "^0.7.8",
|
||||
"@hcengineering/server-core": "^0.7.8",
|
||||
@@ -89,7 +92,6 @@
|
||||
"mongodb": "^6.16.0",
|
||||
"openai": "^4.56.0",
|
||||
"uuid": "^8.3.2",
|
||||
"ws": "^8.18.2",
|
||||
"@hcengineering/analytics-service": "^0.7.5"
|
||||
"ws": "^8.18.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
//
|
||||
// Copyright © 2025 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 { groupByArray, MeasureContext, systemAccountUuid, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import {
|
||||
getClient as getBillingClient,
|
||||
type BillingClient,
|
||||
AiTranscriptData,
|
||||
AiTokensData
|
||||
} from '@hcengineering/billing-client'
|
||||
import { withRetry } from '@hcengineering/retry'
|
||||
|
||||
import config from './config'
|
||||
|
||||
interface DeepgramRequest {
|
||||
request_id: string
|
||||
path: string
|
||||
created: string
|
||||
response?: {
|
||||
details?: {
|
||||
usd?: number
|
||||
duration?: number
|
||||
metadata?: Record<string, any>
|
||||
tags?: string[]
|
||||
}
|
||||
code: number
|
||||
completed: string
|
||||
}
|
||||
}
|
||||
|
||||
interface DeepgramRequestsResponse {
|
||||
page: number
|
||||
limit: number
|
||||
requests?: DeepgramRequest[]
|
||||
}
|
||||
|
||||
export interface TranscriptData {
|
||||
workspace: WorkspaceUuid
|
||||
day: string
|
||||
requestId: string
|
||||
startTime: string
|
||||
durationSeconds: number
|
||||
usd: number
|
||||
}
|
||||
|
||||
async function fetchDeepgramRequests (
|
||||
ctx: MeasureContext,
|
||||
start?: Date,
|
||||
end?: Date,
|
||||
page?: number
|
||||
): Promise<DeepgramRequestsResponse> {
|
||||
const url = new URL(`https://api.deepgram.com/v1/projects/${config.DeepgramProjectId}/requests`)
|
||||
if (start != null) {
|
||||
url.searchParams.set('start', start.toISOString())
|
||||
}
|
||||
if (end != null) {
|
||||
url.searchParams.set('end', end.toISOString())
|
||||
}
|
||||
|
||||
if (page != null) {
|
||||
url.searchParams.set('page', page.toString())
|
||||
}
|
||||
|
||||
url.searchParams.set('limit', '100')
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: { Authorization: `Token ${config.DeepgramApiKey}` }
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text()
|
||||
ctx.error('Failed to fetch deepgram requests', { status: res.status, text })
|
||||
throw new Error(`Failed to fetch deepgram requests ${res.status}: ${text}`)
|
||||
}
|
||||
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
function extractExtra (path: string): Record<string, any> {
|
||||
try {
|
||||
const query = path.split('?')[1]
|
||||
if (query == null) return {}
|
||||
const params = new URLSearchParams(query)
|
||||
const extras = params.getAll('extra')
|
||||
return Object.fromEntries(extras.map((pair) => pair.split(':', 2)).filter(([k, v]) => k != null && v != null))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function extractWorkspace (req: DeepgramRequest): WorkspaceUuid | undefined {
|
||||
const metadata = req.response?.details?.metadata ?? {}
|
||||
if (metadata?.workspace != null && metadata.workspace !== '') return metadata.workspace
|
||||
const extra = extractExtra(req.path ?? '')
|
||||
const ws = extra.workspace
|
||||
|
||||
if (ws == null) return undefined
|
||||
if (typeof ws !== 'string') return undefined
|
||||
if (ws.trim() === '') return undefined
|
||||
return ws as WorkspaceUuid
|
||||
}
|
||||
|
||||
async function fetchLastData (ctx: MeasureContext, billingClient: BillingClient): Promise<AiTranscriptData | undefined> {
|
||||
try {
|
||||
return await billingClient.getAiTranscriptLastData()
|
||||
} catch (e: any) {
|
||||
if (e.name === 'NetworkError') {
|
||||
throw e
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateDeepgramBilling (ctx: MeasureContext): Promise<void> {
|
||||
if (config.DeepgramApiKey === '' || config.DeepgramProjectId === '' || config.DeepgramTag === '') return
|
||||
ctx.info('Starting deepgram billing update')
|
||||
|
||||
const token = generateToken(systemAccountUuid, undefined, { service: 'ai-bot' })
|
||||
const billingClient = getBillingClient(config.BillingUrl, token)
|
||||
|
||||
const lastData = await withRetry(() => fetchLastData(ctx, billingClient))
|
||||
ctx.info('Last deepgram request', lastData)
|
||||
|
||||
const start = lastData != null ? new Date(lastData.lastStartTime) : undefined
|
||||
const end = new Date()
|
||||
let page = 0
|
||||
|
||||
const data: TranscriptData[] = []
|
||||
|
||||
while (true) {
|
||||
const res = await withRetry(() => fetchDeepgramRequests(ctx, start, end, page))
|
||||
const requests = res.requests ?? []
|
||||
|
||||
for (const req of requests) {
|
||||
if (lastData != null && lastData.lastRequestId === req.request_id) continue
|
||||
|
||||
const tags = req.response?.details?.tags ?? []
|
||||
if (!tags.includes(config.DeepgramTag)) continue
|
||||
const workspace = extractWorkspace(req)
|
||||
if (workspace == null) continue
|
||||
|
||||
const endTime = req.response?.completed
|
||||
const durationSeconds = req.response?.details?.duration
|
||||
const usd = req.response?.details?.usd
|
||||
|
||||
if (endTime == null || durationSeconds == null || usd == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
const day = new Date(req.created)
|
||||
day.setHours(0, 0, 0, 0)
|
||||
|
||||
data.push({
|
||||
workspace,
|
||||
day: day.toISOString(),
|
||||
requestId: req.request_id,
|
||||
startTime: req.created,
|
||||
durationSeconds,
|
||||
usd
|
||||
})
|
||||
}
|
||||
|
||||
if (requests.length < res.limit) break
|
||||
|
||||
page++
|
||||
}
|
||||
|
||||
const groupped = groupByArray(data, (it) => `${it.workspace}:${it.day}`)
|
||||
const requestData: AiTranscriptData[] = []
|
||||
|
||||
for (const [, values] of groupped.entries()) {
|
||||
if (values.length === 0) continue
|
||||
|
||||
const last = values.reduce((a, b) => (new Date(a.startTime).getTime() > new Date(b.startTime).getTime() ? a : b))
|
||||
|
||||
const totalDurationSeconds = values.reduce((sum, it) => sum + (it.durationSeconds ?? 0), 0)
|
||||
const totalUsd = values.reduce((sum, it) => sum + (it.usd ?? 0), 0)
|
||||
|
||||
const req: AiTranscriptData = {
|
||||
workspace: values[0].workspace,
|
||||
day: values[0].day,
|
||||
lastRequestId: last.requestId,
|
||||
lastStartTime: last.startTime,
|
||||
durationSeconds: totalDurationSeconds,
|
||||
usd: totalUsd
|
||||
}
|
||||
requestData.push(req)
|
||||
}
|
||||
|
||||
if (requestData.length > 0) {
|
||||
await billingClient.postAiTranscriptData(requestData)
|
||||
}
|
||||
|
||||
ctx.info('Finished deepgram billing update')
|
||||
}
|
||||
|
||||
export async function pushTokensData (ctx: MeasureContext, data: AiTokensData[]): Promise<void> {
|
||||
if (config.BillingUrl === '') return
|
||||
try {
|
||||
const token = generateToken(systemAccountUuid, undefined, { service: 'ai-bot' })
|
||||
const billingClient = getBillingClient(config.BillingUrl, token)
|
||||
await billingClient.postAiTokensData(data)
|
||||
} catch (e) {
|
||||
ctx.error('Failed to push tokens data', { e })
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,11 @@ interface Config {
|
||||
Port: number
|
||||
LoveEndpoint: string
|
||||
DataLabApiKey: string
|
||||
BillingUrl: string
|
||||
DeepgramPollIntervalMinutes: number
|
||||
DeepgramApiKey: string
|
||||
DeepgramProjectId: string
|
||||
DeepgramTag: string
|
||||
}
|
||||
|
||||
const parseNumber = (str: string | undefined): number | undefined => (str !== undefined ? Number(str) : undefined)
|
||||
@@ -63,7 +68,12 @@ const config: Config = (() => {
|
||||
MaxHistoryRecords: parseNumber(process.env.MAX_HISTORY_RECORDS) ?? 500,
|
||||
Port: parseNumber(process.env.PORT) ?? 4010,
|
||||
LoveEndpoint: process.env.LOVE_ENDPOINT ?? '',
|
||||
DataLabApiKey: process.env.DATALAB_API_KEY ?? ''
|
||||
DataLabApiKey: process.env.DATALAB_API_KEY ?? '',
|
||||
BillingUrl: process.env.BILLING_URL ?? '',
|
||||
DeepgramPollIntervalMinutes: parseNumber(process.env.DEEPGRAM_POLL_INTERVAL_MINUTES) ?? 60,
|
||||
DeepgramApiKey: process.env.DEEPGRAM_API_KEY ?? '',
|
||||
DeepgramProjectId: process.env.DEEPGRAM_PROJECT_ID ?? '',
|
||||
DeepgramTag: process.env.DEEPGRAM_TAG ?? ''
|
||||
}
|
||||
|
||||
const missingEnv = (Object.keys(params) as Array<keyof Config>).filter((key) => params[key] === undefined)
|
||||
|
||||
@@ -218,12 +218,12 @@ export class AIControl {
|
||||
return this.workspaces.get(workspace)
|
||||
}
|
||||
|
||||
async translate (req: TranslateRequest): Promise<TranslateResponse | undefined> {
|
||||
async translate (workspace: WorkspaceUuid, req: TranslateRequest): Promise<TranslateResponse | undefined> {
|
||||
if (this.openai === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const html = jsonToHTML(markupToJSON(req.text))
|
||||
const result = await translateHtml(this.openai, html, req.lang)
|
||||
const result = await translateHtml(this.ctx, workspace, this.openai, html, req.lang)
|
||||
const text = result !== undefined ? htmlToMarkup(result) : req.text
|
||||
return {
|
||||
text,
|
||||
@@ -302,7 +302,7 @@ export class AIControl {
|
||||
}
|
||||
}
|
||||
|
||||
const summary = await summarizeMessages(this.openai, messagesToSummarize, req.lang)
|
||||
const summary = await summarizeMessages(this.ctx, workspace, this.openai, messagesToSummarize, req.lang)
|
||||
if (summary === undefined) return
|
||||
|
||||
const summaryMarkup = jsonToMarkup(markdownToMarkup(summary))
|
||||
|
||||
@@ -60,11 +60,11 @@ export function createServer (controller: AIControl, ctx: MeasureContext): Expre
|
||||
|
||||
app.post(
|
||||
'/translate',
|
||||
wrapRequest(async (req, res) => {
|
||||
wrapRequest(async (req, res, token) => {
|
||||
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)
|
||||
const response = await controller.translate(token.workspace, req.body as TranslateRequest)
|
||||
if (response === undefined) {
|
||||
throw new ApiError(500)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user