mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 09:20:54 +00:00
New Indexer fixes (#2546)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
0cd868ce4a
commit
0235aa5007
@ -51,7 +51,6 @@ export class TAttachment extends TAttachedDoc implements Attachment {
|
|||||||
size!: number
|
size!: number
|
||||||
|
|
||||||
@Prop(TypeString(), attachment.string.Type)
|
@Prop(TypeString(), attachment.string.Type)
|
||||||
@Index(IndexKind.FullText)
|
|
||||||
type!: string
|
type!: string
|
||||||
|
|
||||||
@Prop(TypeTimestamp(), attachment.string.Date)
|
@Prop(TypeTimestamp(), attachment.string.Date)
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
FullTextData,
|
FullTextData,
|
||||||
FullTextSearchContext,
|
FullTextSearchContext,
|
||||||
IndexKind,
|
IndexKind,
|
||||||
|
IndexStageState,
|
||||||
Interface,
|
Interface,
|
||||||
Mixin,
|
Mixin,
|
||||||
Obj,
|
Obj,
|
||||||
@ -251,7 +252,13 @@ export class TDocIndexState extends TDoc implements DocIndexState {
|
|||||||
removed!: boolean
|
removed!: boolean
|
||||||
|
|
||||||
// States for diffetent stages
|
// States for diffetent stages
|
||||||
stages!: Record<string, boolean>
|
stages!: Record<string, boolean | string>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Model(core.class.IndexStageState, core.class.Doc, DOMAIN_DOC_INDEX_STATE)
|
||||||
|
export class TIndexStageState extends TDoc implements IndexStageState {
|
||||||
|
stageId!: string
|
||||||
|
attributes!: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
@MMixin(core.mixin.FullTextSearchContext, core.class.Class)
|
@MMixin(core.mixin.FullTextSearchContext, core.class.Class)
|
||||||
|
@ -46,7 +46,8 @@ import {
|
|||||||
TTypeRelatedDocument,
|
TTypeRelatedDocument,
|
||||||
TTypeString,
|
TTypeString,
|
||||||
TTypeTimestamp,
|
TTypeTimestamp,
|
||||||
TVersion
|
TVersion,
|
||||||
|
TIndexStageState
|
||||||
} from './core'
|
} from './core'
|
||||||
import { TAccount, TSpace } from './security'
|
import { TAccount, TSpace } from './security'
|
||||||
import { TUserStatus } from './transient'
|
import { TUserStatus } from './transient'
|
||||||
@ -99,6 +100,7 @@ export function createModel (builder: Builder): void {
|
|||||||
TFulltextData,
|
TFulltextData,
|
||||||
TTypeRelatedDocument,
|
TTypeRelatedDocument,
|
||||||
TDocIndexState,
|
TDocIndexState,
|
||||||
|
TIndexStageState,
|
||||||
TFullTextSearchContext,
|
TFullTextSearchContext,
|
||||||
TConfiguration,
|
TConfiguration,
|
||||||
TConfigurationElement
|
TConfigurationElement
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
Mixin,
|
Mixin,
|
||||||
Model,
|
Model,
|
||||||
Prop,
|
Prop,
|
||||||
|
ReadOnly,
|
||||||
TypeBoolean,
|
TypeBoolean,
|
||||||
TypeDate,
|
TypeDate,
|
||||||
TypeMarkup,
|
TypeMarkup,
|
||||||
@ -34,14 +35,22 @@ import attachment from '@hcengineering/model-attachment'
|
|||||||
import calendar from '@hcengineering/model-calendar'
|
import calendar from '@hcengineering/model-calendar'
|
||||||
import chunter from '@hcengineering/model-chunter'
|
import chunter from '@hcengineering/model-chunter'
|
||||||
import contact, { TOrganization, TPerson } from '@hcengineering/model-contact'
|
import contact, { TOrganization, TPerson } from '@hcengineering/model-contact'
|
||||||
import core, { TSpace } from '@hcengineering/model-core'
|
import core, { TAttachedDoc, TSpace } from '@hcengineering/model-core'
|
||||||
import presentation from '@hcengineering/model-presentation'
|
import presentation from '@hcengineering/model-presentation'
|
||||||
import tags from '@hcengineering/model-tags'
|
import tags from '@hcengineering/model-tags'
|
||||||
import task, { actionTemplates, TSpaceWithStates, TTask } from '@hcengineering/model-task'
|
import task, { actionTemplates, DOMAIN_TASK, TSpaceWithStates, TTask } from '@hcengineering/model-task'
|
||||||
import view, { actionTemplates as viewTemplates, createAction } from '@hcengineering/model-view'
|
import view, { actionTemplates as viewTemplates, createAction } from '@hcengineering/model-view'
|
||||||
import workbench, { Application, createNavigateAction } from '@hcengineering/model-workbench'
|
import workbench, { Application, createNavigateAction } from '@hcengineering/model-workbench'
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||||
import { Applicant, Candidate, Candidates, recruitId, Vacancy, VacancyList } from '@hcengineering/recruit'
|
import {
|
||||||
|
Applicant,
|
||||||
|
ApplicantMatch,
|
||||||
|
Candidate,
|
||||||
|
Candidates,
|
||||||
|
recruitId,
|
||||||
|
Vacancy,
|
||||||
|
VacancyList
|
||||||
|
} from '@hcengineering/recruit'
|
||||||
import setting from '@hcengineering/setting'
|
import setting from '@hcengineering/setting'
|
||||||
import { KeyBinding } from '@hcengineering/view'
|
import { KeyBinding } from '@hcengineering/view'
|
||||||
import recruit from './plugin'
|
import recruit from './plugin'
|
||||||
@ -104,6 +113,12 @@ export class TCandidate extends TPerson implements Candidate {
|
|||||||
|
|
||||||
@Prop(Collection(recruit.class.Review, recruit.string.Review), recruit.string.Reviews)
|
@Prop(Collection(recruit.class.Review, recruit.string.Review), recruit.string.Reviews)
|
||||||
reviews?: number
|
reviews?: number
|
||||||
|
|
||||||
|
@Prop(
|
||||||
|
Collection(recruit.class.ApplicantMatch, getEmbeddedLabel('Vacancy match')),
|
||||||
|
getEmbeddedLabel('Vacancy Matches')
|
||||||
|
)
|
||||||
|
vacancyMatch?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mixin(recruit.mixin.VacancyList, contact.class.Organization)
|
@Mixin(recruit.mixin.VacancyList, contact.class.Organization)
|
||||||
@ -136,8 +151,33 @@ export class TApplicant extends TTask implements Applicant {
|
|||||||
declare assignee: Ref<Employee> | null
|
declare assignee: Ref<Employee> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Model(recruit.class.ApplicantMatch, core.class.AttachedDoc, DOMAIN_TASK)
|
||||||
|
@UX(recruit.string.Application, recruit.icon.Application, recruit.string.ApplicationShort, 'number')
|
||||||
|
export class TApplicantMatch extends TAttachedDoc implements ApplicantMatch {
|
||||||
|
// We need to declare, to provide property with label
|
||||||
|
@Prop(TypeRef(recruit.mixin.Candidate), recruit.string.Talent)
|
||||||
|
@Index(IndexKind.Indexed)
|
||||||
|
declare attachedTo: Ref<Candidate>
|
||||||
|
|
||||||
|
@Prop(TypeBoolean(), getEmbeddedLabel('Complete'))
|
||||||
|
@ReadOnly()
|
||||||
|
complete!: boolean
|
||||||
|
|
||||||
|
@Prop(TypeString(), getEmbeddedLabel('Vacancy'))
|
||||||
|
@ReadOnly()
|
||||||
|
vacancy!: string
|
||||||
|
|
||||||
|
@Prop(TypeString(), getEmbeddedLabel('Summary'))
|
||||||
|
@ReadOnly()
|
||||||
|
summary!: string
|
||||||
|
|
||||||
|
@Prop(TypeMarkup(), getEmbeddedLabel('Response'))
|
||||||
|
@ReadOnly()
|
||||||
|
response!: string
|
||||||
|
}
|
||||||
|
|
||||||
export function createModel (builder: Builder): void {
|
export function createModel (builder: Builder): void {
|
||||||
builder.createModel(TVacancy, TCandidates, TCandidate, TApplicant, TReview, TOpinion, TVacancyList)
|
builder.createModel(TVacancy, TCandidates, TCandidate, TApplicant, TReview, TOpinion, TVacancyList, TApplicantMatch)
|
||||||
|
|
||||||
builder.mixin(recruit.class.Vacancy, core.class.Class, workbench.mixin.SpaceView, {
|
builder.mixin(recruit.class.Vacancy, core.class.Class, workbench.mixin.SpaceView, {
|
||||||
view: {
|
view: {
|
||||||
@ -363,7 +403,7 @@ export function createModel (builder: Builder): void {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
recruit.viewlet.StatusTableApplicant
|
recruit.viewlet.TableApplicant
|
||||||
)
|
)
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
view.class.Viewlet,
|
view.class.Viewlet,
|
||||||
@ -386,7 +426,19 @@ export function createModel (builder: Builder): void {
|
|||||||
],
|
],
|
||||||
hiddenKeys: ['name']
|
hiddenKeys: ['name']
|
||||||
},
|
},
|
||||||
recruit.viewlet.TableApplicant
|
recruit.viewlet.ApplicantTable
|
||||||
|
)
|
||||||
|
|
||||||
|
builder.createDoc(
|
||||||
|
view.class.Viewlet,
|
||||||
|
core.space.Model,
|
||||||
|
{
|
||||||
|
attachTo: recruit.class.ApplicantMatch,
|
||||||
|
descriptor: view.viewlet.Table,
|
||||||
|
config: ['', 'response', 'attachedTo', 'space', 'modifiedOn'],
|
||||||
|
hiddenKeys: []
|
||||||
|
},
|
||||||
|
recruit.viewlet.TableApplicantMatch
|
||||||
)
|
)
|
||||||
|
|
||||||
const applicantKanbanLookup: Lookup<Applicant> = {
|
const applicantKanbanLookup: Lookup<Applicant> = {
|
||||||
@ -445,6 +497,14 @@ export function createModel (builder: Builder): void {
|
|||||||
presenter: recruit.component.ApplicationsPresenter
|
presenter: recruit.component.ApplicationsPresenter
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.mixin(recruit.class.ApplicantMatch, core.class.Class, view.mixin.ObjectPresenter, {
|
||||||
|
presenter: recruit.component.ApplicationMatchPresenter
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(recruit.class.ApplicantMatch, core.class.Class, view.mixin.CollectionPresenter, {
|
||||||
|
presenter: recruit.component.ApplicationMatchPresenter
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ObjectPresenter, {
|
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ObjectPresenter, {
|
||||||
presenter: recruit.component.VacancyPresenter
|
presenter: recruit.component.VacancyPresenter
|
||||||
})
|
})
|
||||||
@ -643,6 +703,15 @@ export function createModel (builder: Builder): void {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createAction(builder, {
|
||||||
|
...viewTemplates.open,
|
||||||
|
target: recruit.class.ApplicantMatch,
|
||||||
|
context: {
|
||||||
|
mode: ['browser', 'context'],
|
||||||
|
group: 'create'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function createGotoSpecialAction (builder: Builder, id: string, key: KeyBinding, label: IntlString): void {
|
function createGotoSpecialAction (builder: Builder, id: string, key: KeyBinding, label: IntlString): void {
|
||||||
createNavigateAction(builder, key, label, recruit.app.Recruit as Ref<Application>, {
|
createNavigateAction(builder, key, label, recruit.app.Recruit as Ref<Application>, {
|
||||||
application: recruitId,
|
application: recruitId,
|
||||||
@ -822,6 +891,32 @@ export function createModel (builder: Builder): void {
|
|||||||
},
|
},
|
||||||
recruit.filter.None
|
recruit.filter.None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Allow to use fuzzy search for mixins
|
||||||
|
builder.mixin(recruit.class.Vacancy, core.class.Class, core.mixin.FullTextSearchContext, {
|
||||||
|
fullTextSummary: true
|
||||||
|
})
|
||||||
|
|
||||||
|
createAction(builder, {
|
||||||
|
label: recruit.string.MatchVacancy,
|
||||||
|
icon: recruit.icon.Vacancy,
|
||||||
|
action: view.actionImpl.ShowPopup,
|
||||||
|
actionProps: {
|
||||||
|
component: recruit.component.MatchVacancy,
|
||||||
|
element: 'top',
|
||||||
|
fillProps: {
|
||||||
|
_objects: 'objects'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: 'any',
|
||||||
|
category: recruit.category.Recruit,
|
||||||
|
keyBinding: [],
|
||||||
|
target: recruit.mixin.Candidate,
|
||||||
|
context: {
|
||||||
|
mode: ['context', 'browser'],
|
||||||
|
group: 'create'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { recruitOperation } from './migration'
|
export { recruitOperation } from './migration'
|
||||||
|
@ -88,7 +88,10 @@ export default mergeIds(recruitId, recruit, {
|
|||||||
NewCandidateHeader: '' as AnyComponent,
|
NewCandidateHeader: '' as AnyComponent,
|
||||||
ApplicantFilter: '' as AnyComponent,
|
ApplicantFilter: '' as AnyComponent,
|
||||||
VacancyList: '' as AnyComponent,
|
VacancyList: '' as AnyComponent,
|
||||||
VacancyTemplateEditor: '' as AnyComponent
|
VacancyTemplateEditor: '' as AnyComponent,
|
||||||
|
ApplicationMatchPresenter: '' as AnyComponent,
|
||||||
|
|
||||||
|
MatchVacancy: '' as AnyComponent
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
DefaultVacancy: '' as Ref<KanbanTemplate>,
|
DefaultVacancy: '' as Ref<KanbanTemplate>,
|
||||||
@ -103,8 +106,9 @@ export default mergeIds(recruitId, recruit, {
|
|||||||
viewlet: {
|
viewlet: {
|
||||||
TableCandidate: '' as Ref<Viewlet>,
|
TableCandidate: '' as Ref<Viewlet>,
|
||||||
TableVacancy: '' as Ref<Viewlet>,
|
TableVacancy: '' as Ref<Viewlet>,
|
||||||
StatusTableApplicant: '' as Ref<Viewlet>,
|
ApplicantTable: '' as Ref<Viewlet>,
|
||||||
TableApplicant: '' as Ref<Viewlet>,
|
TableApplicant: '' as Ref<Viewlet>,
|
||||||
|
TableApplicantMatch: '' as Ref<Viewlet>,
|
||||||
CalendarReview: '' as Ref<Viewlet>,
|
CalendarReview: '' as Ref<Viewlet>,
|
||||||
TableReview: '' as Ref<Viewlet>
|
TableReview: '' as Ref<Viewlet>
|
||||||
}
|
}
|
||||||
|
@ -350,7 +350,7 @@ export interface DocIndexState extends Doc {
|
|||||||
attachedToClass?: Ref<Class<Doc>>
|
attachedToClass?: Ref<Class<Doc>>
|
||||||
|
|
||||||
// States for stages
|
// States for stages
|
||||||
stages: Record<string, boolean>
|
stages: Record<string, boolean | string>
|
||||||
|
|
||||||
removed: boolean
|
removed: boolean
|
||||||
|
|
||||||
@ -362,6 +362,14 @@ export interface DocIndexState extends Doc {
|
|||||||
shortSummary?: Markup | null
|
shortSummary?: Markup | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface IndexStageState extends Doc {
|
||||||
|
stageId: string
|
||||||
|
attributes: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*
|
*
|
||||||
|
@ -41,7 +41,8 @@ import type {
|
|||||||
Type,
|
Type,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
Configuration,
|
Configuration,
|
||||||
ConfigurationElement
|
ConfigurationElement,
|
||||||
|
IndexStageState
|
||||||
} from './classes'
|
} from './classes'
|
||||||
import type {
|
import type {
|
||||||
Tx,
|
Tx,
|
||||||
@ -102,6 +103,7 @@ export default plugin(coreId, {
|
|||||||
FulltextData: '' as Ref<Class<FullTextData>>,
|
FulltextData: '' as Ref<Class<FullTextData>>,
|
||||||
TypeRelatedDocument: '' as Ref<Class<Type<RelatedDocument>>>,
|
TypeRelatedDocument: '' as Ref<Class<Type<RelatedDocument>>>,
|
||||||
DocIndexState: '' as Ref<Class<DocIndexState>>,
|
DocIndexState: '' as Ref<Class<DocIndexState>>,
|
||||||
|
IndexStageState: '' as Ref<Class<IndexStageState>>,
|
||||||
|
|
||||||
Configuration: '' as Ref<Class<Configuration>>
|
Configuration: '' as Ref<Class<Configuration>>
|
||||||
},
|
},
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
let search = ''
|
let search = ''
|
||||||
|
|
||||||
$: summary = (indexDoc?.attributes as any).summary
|
$: summary = indexDoc?.fullSummary ?? undefined
|
||||||
|
|
||||||
$: attributes =
|
$: attributes =
|
||||||
indexDoc !== undefined
|
indexDoc !== undefined
|
||||||
@ -51,10 +51,17 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Panel on:changeContent on:close>
|
<Panel on:changeContent on:close>
|
||||||
<EditBox bind:value={search} kind="search-style" />
|
<EditBox focus bind:value={search} kind="search-style" />
|
||||||
<div class="indexed-background">
|
<div class="indexed-background">
|
||||||
<div class="indexed-doc text-base max-h-125">
|
<div class="indexed-doc text-base max-h-125">
|
||||||
{#if summary}
|
{#if summary}
|
||||||
|
{#if search.length > 0}
|
||||||
|
Result:
|
||||||
|
{#each summary.split('\n').filter((line) => line.toLowerCase().includes(search.toLowerCase())) as line}
|
||||||
|
<span class:highlight={true}>{line}</span>
|
||||||
|
{/each}
|
||||||
|
<br />
|
||||||
|
{/if}
|
||||||
Summary:
|
Summary:
|
||||||
{#each summary.split('\n') as line}
|
{#each summary.split('\n') as line}
|
||||||
{@const hl = search.length > 0 && line.toLowerCase().includes(search.toLowerCase())}
|
{@const hl = search.length > 0 && line.toLowerCase().includes(search.toLowerCase())}
|
||||||
@ -74,6 +81,13 @@
|
|||||||
<div class="p-1 flex-row flex-wrap">
|
<div class="p-1 flex-row flex-wrap">
|
||||||
{#each attr[1] as doc}
|
{#each attr[1] as doc}
|
||||||
<div class="p-1" class:flex-col={doc.length > 1}>
|
<div class="p-1" class:flex-col={doc.length > 1}>
|
||||||
|
{#if search.length > 0}
|
||||||
|
Result:
|
||||||
|
{#each doc.filter((line) => line.toLowerCase().includes(search.toLowerCase())) as line}
|
||||||
|
<span class:highlight={true}>{line}</span>
|
||||||
|
{/each}
|
||||||
|
<br />
|
||||||
|
{/if}
|
||||||
{#each doc as line}
|
{#each doc as line}
|
||||||
{@const hl = search.length > 0 && line.toLowerCase().includes(search.toLowerCase())}
|
{@const hl = search.length > 0 && line.toLowerCase().includes(search.toLowerCase())}
|
||||||
<span class:text-md={!hl} class:highlight={hl}>{line}</span>
|
<span class:text-md={!hl} class:highlight={hl}>{line}</span>
|
||||||
@ -100,7 +114,7 @@
|
|||||||
color: black;
|
color: black;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
.highlight {
|
.highlight {
|
||||||
color: red;
|
color: blue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -13,18 +13,18 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { IconFolder, Label } from '@hcengineering/ui'
|
import { IconFolder, IconSize, Label } from '@hcengineering/ui'
|
||||||
|
|
||||||
import type { Space } from '@hcengineering/core'
|
import type { Space } from '@hcengineering/core'
|
||||||
import presentation from '..'
|
import presentation from '..'
|
||||||
|
|
||||||
export let value: Space
|
export let value: Space
|
||||||
export let subtitle: string | undefined = undefined
|
export let subtitle: string | undefined = undefined
|
||||||
export let size: 'medium' | 'large'
|
export let size: IconSize
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-row-center">
|
<div class="flex-row-center">
|
||||||
<div class="flex-center {size} caption-color flex-no-shrink"><IconFolder size={'small'} /></div>
|
<div class="flex-center caption-color flex-no-shrink"><IconFolder {size} /></div>
|
||||||
<div class="flex-col ml-2 min-w-0">
|
<div class="flex-col ml-2 min-w-0">
|
||||||
{#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if}
|
{#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if}
|
||||||
<div class="content-accent-color overflow-label">
|
<div class="content-accent-color overflow-label">
|
||||||
|
@ -45,13 +45,14 @@
|
|||||||
export let create: ObjectCreate | undefined = undefined
|
export let create: ObjectCreate | undefined = undefined
|
||||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
export let labelDirection: TooltipAlignment | undefined = undefined
|
||||||
export let kind: ButtonKind = 'no-border'
|
export let kind: ButtonKind = 'no-border'
|
||||||
export let size: ButtonSize = 'small'
|
export let size: ButtonSize = 'large'
|
||||||
export let justify: 'left' | 'center' = 'center'
|
export let justify: 'left' | 'center' = 'center'
|
||||||
export let width: string | undefined = undefined
|
export let width: string | undefined = undefined
|
||||||
export let allowDeselect = false
|
export let allowDeselect = false
|
||||||
export let component: AnySvelteComponent | undefined = undefined
|
export let component: AnySvelteComponent | undefined = undefined
|
||||||
export let componentProps: any | undefined = undefined
|
export let componentProps: any | undefined = undefined
|
||||||
export let autoSelect = true
|
export let autoSelect = true
|
||||||
|
export let readonly = false
|
||||||
|
|
||||||
let selected: Space | undefined
|
let selected: Space | undefined
|
||||||
|
|
||||||
@ -75,11 +76,15 @@
|
|||||||
$: updateSelected(value)
|
$: updateSelected(value)
|
||||||
|
|
||||||
const showSpacesPopup = (ev: MouseEvent) => {
|
const showSpacesPopup = (ev: MouseEvent) => {
|
||||||
|
if (readonly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
showPopup(
|
showPopup(
|
||||||
SpacesPopup,
|
SpacesPopup,
|
||||||
{
|
{
|
||||||
_class,
|
_class,
|
||||||
label,
|
label,
|
||||||
|
size,
|
||||||
allowDeselect,
|
allowDeselect,
|
||||||
spaceOptions: { ...(spaceOptions ?? {}), sort: { ...(spaceOptions?.sort ?? {}), modifiedOn: -1 } },
|
spaceOptions: { ...(spaceOptions ?? {}), sort: { ...(spaceOptions?.sort ?? {}), modifiedOn: -1 } },
|
||||||
selected: selected?._id,
|
selected: selected?._id,
|
||||||
@ -109,6 +114,7 @@
|
|||||||
<Button
|
<Button
|
||||||
id="space.selector"
|
id="space.selector"
|
||||||
{focus}
|
{focus}
|
||||||
|
disabled={readonly}
|
||||||
{focusIndex}
|
{focusIndex}
|
||||||
icon={IconFolder}
|
icon={IconFolder}
|
||||||
{size}
|
{size}
|
||||||
@ -118,7 +124,7 @@
|
|||||||
showTooltip={{ label, direction: labelDirection }}
|
showTooltip={{ label, direction: labelDirection }}
|
||||||
on:click={showSpacesPopup}
|
on:click={showSpacesPopup}
|
||||||
>
|
>
|
||||||
<span slot="content" class="overflow-label disabled text-sm" class:dark-color={value == null}>
|
<span slot="content" class="overflow-label disabled text" class:dark-color={value == null}>
|
||||||
{#if selected}{selected.name}{:else}<Label {label} />{/if}
|
{#if selected}{selected.name}{:else}<Label {label} />{/if}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
|
import type { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
|
||||||
import { AnySvelteComponent } from '@hcengineering/ui'
|
import { AnySvelteComponent, ButtonSize } from '@hcengineering/ui'
|
||||||
import { ObjectCreate } from '../types'
|
import { ObjectCreate } from '../types'
|
||||||
import ObjectPopup from './ObjectPopup.svelte'
|
import ObjectPopup from './ObjectPopup.svelte'
|
||||||
import SpaceInfo from './SpaceInfo.svelte'
|
import SpaceInfo from './SpaceInfo.svelte'
|
||||||
@ -24,6 +24,7 @@
|
|||||||
export let spaceQuery: DocumentQuery<Space> | undefined
|
export let spaceQuery: DocumentQuery<Space> | undefined
|
||||||
export let spaceOptions: FindOptions<Space> | undefined = {}
|
export let spaceOptions: FindOptions<Space> | undefined = {}
|
||||||
export let create: ObjectCreate | undefined = undefined
|
export let create: ObjectCreate | undefined = undefined
|
||||||
|
export let size: ButtonSize = 'large'
|
||||||
export let allowDeselect = false
|
export let allowDeselect = false
|
||||||
export let component: AnySvelteComponent | undefined = undefined
|
export let component: AnySvelteComponent | undefined = undefined
|
||||||
export let componentProps: any | undefined = undefined
|
export let componentProps: any | undefined = undefined
|
||||||
@ -51,9 +52,9 @@
|
|||||||
>
|
>
|
||||||
<svelte:fragment slot="item" let:item={space}>
|
<svelte:fragment slot="item" let:item={space}>
|
||||||
{#if component}
|
{#if component}
|
||||||
<svelte:component this={component} {...componentProps} size={'large'} value={space} />
|
<svelte:component this={component} {...componentProps} {size} value={space} />
|
||||||
{:else}
|
{:else}
|
||||||
<SpaceInfo size={'large'} value={space} />
|
<SpaceInfo {size} value={space} />
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ObjectPopup>
|
</ObjectPopup>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let size: 'small' | 'medium' | 'large'
|
import { IconSize } from '../../types'
|
||||||
|
export let size: IconSize
|
||||||
const fill: string = 'currentColor'
|
const fill: string = 'currentColor'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
|
|
||||||
<div class="flex-row-center attachment-container">
|
<div class="flex-row-center attachment-container">
|
||||||
{#if openEmbedded(value.type)}
|
{#if openEmbedded(value.type)}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="flex-center icon"
|
class="flex-center icon"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
@ -71,6 +72,7 @@
|
|||||||
<div class="flex-center icon">
|
<div class="flex-center icon">
|
||||||
{iconLabel(value.name)}
|
{iconLabel(value.name)}
|
||||||
{#if removable}
|
{#if removable}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="remove-btn"
|
class="remove-btn"
|
||||||
on:click={(ev) => {
|
on:click={(ev) => {
|
||||||
@ -86,6 +88,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex-col info">
|
<div class="flex-col info">
|
||||||
{#if openEmbedded(value.type)}
|
{#if openEmbedded(value.type)}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="name"
|
class="name"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
|
@ -26,12 +26,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if value && value > 0}
|
{#if value && value > 0}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
use:tooltip={{
|
use:tooltip={{
|
||||||
label: attachment.string.Attachments,
|
label: attachment.string.Attachments,
|
||||||
component: AttachmentPopup,
|
component: AttachmentPopup,
|
||||||
props: { objectId: object._id, attachments: value }
|
props: { objectId: object._id, attachments: value }
|
||||||
}}
|
}}
|
||||||
|
on:click|preventDefault|stopPropagation={() => {}}
|
||||||
class="sm-tool-icon ml-1 mr-1"
|
class="sm-tool-icon ml-1 mr-1"
|
||||||
>
|
>
|
||||||
<span class="icon"><IconAttachment {size} /></span>
|
<span class="icon"><IconAttachment {size} /></span>
|
||||||
|
@ -26,12 +26,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if value && value > 0}
|
{#if value && value > 0}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
use:tooltip={{
|
use:tooltip={{
|
||||||
label: chunter.string.Comments,
|
label: chunter.string.Comments,
|
||||||
component: CommentPopup,
|
component: CommentPopup,
|
||||||
props: { objectId: object._id }
|
props: { objectId: object._id }
|
||||||
}}
|
}}
|
||||||
|
on:click|preventDefault|stopPropagation={() => {}}
|
||||||
class="sm-tool-icon ml-1 mr-1"
|
class="sm-tool-icon ml-1 mr-1"
|
||||||
>
|
>
|
||||||
<span class="icon"><IconThread {size} /></span>
|
<span class="icon"><IconThread {size} /></span>
|
||||||
|
@ -99,7 +99,12 @@
|
|||||||
"HasNoActiveApplicant": "No Active",
|
"HasNoActiveApplicant": "No Active",
|
||||||
"NoneApplications": "None",
|
"NoneApplications": "None",
|
||||||
"RelatedIssues": "Related issues",
|
"RelatedIssues": "Related issues",
|
||||||
"VacancyList": "Vacancies"
|
"VacancyList": "Vacancies",
|
||||||
|
"MatchVacancy": "Match to vacancy",
|
||||||
|
"VacancyMatching": "Match Talents to vacancy",
|
||||||
|
"Score": "Score",
|
||||||
|
"Match": "Match",
|
||||||
|
"PerformMatch": "Match"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"TalentRequired": "Please select talent",
|
"TalentRequired": "Please select talent",
|
||||||
|
@ -101,7 +101,12 @@
|
|||||||
"HasNoActiveApplicant": "Не активные",
|
"HasNoActiveApplicant": "Не активные",
|
||||||
"NoneApplications": "Отсутствуют",
|
"NoneApplications": "Отсутствуют",
|
||||||
"RelatedIssues": "Связанные задачи",
|
"RelatedIssues": "Связанные задачи",
|
||||||
"VacancyList": "Вакансии"
|
"VacancyList": "Вакансии",
|
||||||
|
"MatchVacancy": "Проверить на вакансию",
|
||||||
|
"VacancyMatching": "Подбор кандидатов на вакансию",
|
||||||
|
"Score": "Оценка",
|
||||||
|
"Match": "Совпадение",
|
||||||
|
"PerformMatch": "Сопоставить"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"TalentRequired": "Пожалуйста выберите таланта",
|
"TalentRequired": "Пожалуйста выберите таланта",
|
||||||
|
@ -13,10 +13,23 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
||||||
|
import chunter from '@hcengineering/chunter'
|
||||||
import type { Contact, Employee, Person } from '@hcengineering/contact'
|
import type { Contact, Employee, Person } from '@hcengineering/contact'
|
||||||
import contact from '@hcengineering/contact'
|
import contact from '@hcengineering/contact'
|
||||||
import ExpandRightDouble from '@hcengineering/contact-resources/src/components/icons/ExpandRightDouble.svelte'
|
import ExpandRightDouble from '@hcengineering/contact-resources/src/components/icons/ExpandRightDouble.svelte'
|
||||||
import { Account, Class, Client, Doc, FindOptions, generateId, Ref, SortingOrder, Space } from '@hcengineering/core'
|
import {
|
||||||
|
Account,
|
||||||
|
Class,
|
||||||
|
Client,
|
||||||
|
Doc,
|
||||||
|
FindOptions,
|
||||||
|
generateId,
|
||||||
|
Markup,
|
||||||
|
Ref,
|
||||||
|
SortingOrder,
|
||||||
|
Space
|
||||||
|
} from '@hcengineering/core'
|
||||||
import { getResource, OK, Resource, Severity, Status } from '@hcengineering/platform'
|
import { getResource, OK, Resource, Severity, Status } from '@hcengineering/platform'
|
||||||
import presentation, {
|
import presentation, {
|
||||||
Card,
|
Card,
|
||||||
@ -32,6 +45,7 @@
|
|||||||
Button,
|
Button,
|
||||||
ColorPopup,
|
ColorPopup,
|
||||||
createFocusManager,
|
createFocusManager,
|
||||||
|
deviceOptionsStore as deviceInfo,
|
||||||
FocusHandler,
|
FocusHandler,
|
||||||
getPlatformColor,
|
getPlatformColor,
|
||||||
Label,
|
Label,
|
||||||
@ -44,13 +58,16 @@
|
|||||||
import CandidateCard from './CandidateCard.svelte'
|
import CandidateCard from './CandidateCard.svelte'
|
||||||
import VacancyCard from './VacancyCard.svelte'
|
import VacancyCard from './VacancyCard.svelte'
|
||||||
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
|
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
|
||||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
|
||||||
|
|
||||||
export let space: Ref<SpaceWithStates>
|
export let space: Ref<SpaceWithStates>
|
||||||
export let candidate: Ref<Candidate>
|
export let candidate: Ref<Candidate>
|
||||||
export let assignee: Ref<Employee>
|
export let assignee: Ref<Employee>
|
||||||
|
export let comment: Markup = ''
|
||||||
|
|
||||||
|
$: _comment = comment
|
||||||
|
|
||||||
export let preserveCandidate = false
|
export let preserveCandidate = false
|
||||||
|
export let preserveVacancy = false
|
||||||
|
|
||||||
let status: Status = OK
|
let status: Status = OK
|
||||||
let createMore: boolean = false
|
let createMore: boolean = false
|
||||||
@ -133,12 +150,22 @@
|
|||||||
rank: calcRank(lastOne, undefined),
|
rank: calcRank(lastOne, undefined),
|
||||||
startDate: null,
|
startDate: null,
|
||||||
dueDate: null
|
dueDate: null
|
||||||
}
|
},
|
||||||
|
doc._id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await descriptionBox.createAttachments()
|
||||||
|
|
||||||
|
if (_comment.trim().length > 0) {
|
||||||
|
await client.addCollection(chunter.class.Comment, _space, doc._id, recruit.class.Applicant, 'comments', {
|
||||||
|
message: _comment
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (createMore) {
|
if (createMore) {
|
||||||
// Prepare for next
|
// Prepare for next
|
||||||
_candidate = '' as Ref<Candidate>
|
_candidate = '' as Ref<Candidate>
|
||||||
|
_comment = ''
|
||||||
doc = {
|
doc = {
|
||||||
state: selectedState?._id as Ref<State>,
|
state: selectedState?._id as Ref<State>,
|
||||||
doneState: null,
|
doneState: null,
|
||||||
@ -256,6 +283,8 @@
|
|||||||
let verticalContent: boolean = false
|
let verticalContent: boolean = false
|
||||||
$: verticalContent = $deviceInfo.isMobile && $deviceInfo.isPortrait
|
$: verticalContent = $deviceInfo.isMobile && $deviceInfo.isPortrait
|
||||||
let btn: HTMLButtonElement
|
let btn: HTMLButtonElement
|
||||||
|
|
||||||
|
let descriptionBox: AttachmentStyledBox
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FocusHandler {manager} />
|
<FocusHandler {manager} />
|
||||||
@ -306,6 +335,7 @@
|
|||||||
_class={recruit.class.Vacancy}
|
_class={recruit.class.Vacancy}
|
||||||
spaceQuery={{ archived: false }}
|
spaceQuery={{ archived: false }}
|
||||||
spaceOptions={orgOptions}
|
spaceOptions={orgOptions}
|
||||||
|
readonly={preserveVacancy}
|
||||||
label={recruit.string.Vacancy}
|
label={recruit.string.Vacancy}
|
||||||
create={{
|
create={{
|
||||||
component: recruit.component.CreateVacancy,
|
component: recruit.component.CreateVacancy,
|
||||||
@ -324,6 +354,27 @@
|
|||||||
</SpaceSelect>
|
</SpaceSelect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#key doc._id}
|
||||||
|
<AttachmentStyledBox
|
||||||
|
bind:this={descriptionBox}
|
||||||
|
objectId={doc._id}
|
||||||
|
shouldSaveDraft={false}
|
||||||
|
_class={recruit.class.Applicant}
|
||||||
|
space={_space}
|
||||||
|
alwaysEdit
|
||||||
|
showButtons={false}
|
||||||
|
emphasized
|
||||||
|
bind:content={_comment}
|
||||||
|
placeholder={recruit.string.Description}
|
||||||
|
on:changeSize={() => dispatch('changeContent')}
|
||||||
|
on:attach={(ev) => {
|
||||||
|
if (ev.detail.action === 'saved') {
|
||||||
|
doc.attachments = ev.detail.value
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
<svelte:fragment slot="pool">
|
<svelte:fragment slot="pool">
|
||||||
{#key doc}
|
{#key doc}
|
||||||
<EmployeeBox
|
<EmployeeBox
|
||||||
|
258
plugins/recruit-resources/src/components/MatchVacancy.svelte
Normal file
258
plugins/recruit-resources/src/components/MatchVacancy.svelte
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import contact from '@hcengineering/contact'
|
||||||
|
import core, { Doc, DocIndexState, FindOptions, Ref } from '@hcengineering/core'
|
||||||
|
import presentation, {
|
||||||
|
Card,
|
||||||
|
createQuery,
|
||||||
|
getClient,
|
||||||
|
IndexedDocumentPreview,
|
||||||
|
MessageViewer,
|
||||||
|
SpaceSelect
|
||||||
|
} from '@hcengineering/presentation'
|
||||||
|
import { Applicant, ApplicantMatch, Candidate, Vacancy } from '@hcengineering/recruit'
|
||||||
|
import { Button, IconActivity, IconAdd, Label, resizeObserver, showPopup, tooltip } from '@hcengineering/ui'
|
||||||
|
import Scroller from '@hcengineering/ui/src/components/Scroller.svelte'
|
||||||
|
import { MarkupPreviewPopup, ObjectPresenter } from '@hcengineering/view-resources'
|
||||||
|
import { cosinesim } from '@hcengineering/view-resources/src/utils'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import recruit from '../plugin'
|
||||||
|
import CreateApplication from './CreateApplication.svelte'
|
||||||
|
import VacancyCard from './VacancyCard.svelte'
|
||||||
|
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
|
||||||
|
|
||||||
|
export let objects: Candidate[] | Candidate
|
||||||
|
|
||||||
|
$: _objects = Array.isArray(objects) ? objects : [objects]
|
||||||
|
|
||||||
|
const orgOptions: FindOptions<Vacancy> = {
|
||||||
|
lookup: {
|
||||||
|
company: contact.class.Organization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _space: Ref<Vacancy> | undefined
|
||||||
|
let vacancy: Vacancy | undefined
|
||||||
|
|
||||||
|
const vacancyQuery = createQuery()
|
||||||
|
$: vacancyQuery.query(recruit.class.Vacancy, { _id: _space }, (res) => {
|
||||||
|
;[vacancy] = res
|
||||||
|
})
|
||||||
|
|
||||||
|
const indexDataQuery = createQuery()
|
||||||
|
let state: Map<Ref<Doc>, DocIndexState> = new Map()
|
||||||
|
$: indexDataQuery.query(
|
||||||
|
core.class.DocIndexState,
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: [_space as unknown as Ref<DocIndexState>, ..._objects.map((it) => it._id as unknown as Ref<DocIndexState>)]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
state = new Map(res.map((it) => [it._id, it] ?? []))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
$: vacancyState = state.get(_space as unknown as Ref<DocIndexState>)
|
||||||
|
|
||||||
|
const matchQuery = createQuery()
|
||||||
|
let matches: Map<Ref<Doc>, ApplicantMatch> = new Map()
|
||||||
|
|
||||||
|
$: matchQuery.query(
|
||||||
|
recruit.class.ApplicantMatch,
|
||||||
|
{
|
||||||
|
attachedTo: { $in: [..._objects.map((it) => it._id)] },
|
||||||
|
space: _space
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
matches = new Map(res.map((it) => [it.attachedTo, it] ?? []))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const applicationQuery = createQuery()
|
||||||
|
let applications: Map<Ref<Doc>, Applicant> = new Map()
|
||||||
|
|
||||||
|
$: applicationQuery.query(
|
||||||
|
recruit.class.Applicant,
|
||||||
|
{
|
||||||
|
attachedTo: { $in: [..._objects.map((it) => it._id)] },
|
||||||
|
space: _space
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
applications = new Map(res.map((it) => [it.attachedTo, it] ?? []))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function getEmbedding (doc: DocIndexState): number[] | undefined {
|
||||||
|
for (const [k, v] of Object.entries(doc.attributes)) {
|
||||||
|
if (k.startsWith('openai_embedding_') && doc.attributes[k + '_use'] === true) {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: vacancyEmbedding = vacancyState && getEmbedding(vacancyState)
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const matching = new Set<string>()
|
||||||
|
|
||||||
|
async function requestMatch (doc: Candidate, docState: DocIndexState): Promise<void> {
|
||||||
|
try {
|
||||||
|
matching.add(doc._id)
|
||||||
|
if (_space === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const oldMatch = matches.get(doc._id)
|
||||||
|
if (oldMatch) {
|
||||||
|
await client.remove(oldMatch)
|
||||||
|
}
|
||||||
|
await client.addCollection(recruit.class.ApplicantMatch, _space, doc._id, doc._class, 'vacancyMatch', {
|
||||||
|
complete: false,
|
||||||
|
vacancy: vacancyState?.fullSummary ?? '',
|
||||||
|
summary: docState.fullSummary ?? '',
|
||||||
|
response: ''
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
matching.delete(doc._id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function createApplication (doc: Candidate, match?: ApplicantMatch): Promise<void> {
|
||||||
|
showPopup(
|
||||||
|
CreateApplication,
|
||||||
|
{
|
||||||
|
space: _space,
|
||||||
|
candidate: doc._id,
|
||||||
|
preserveCandidate: true,
|
||||||
|
preserveVacancy: true,
|
||||||
|
comment: match?.response ?? ''
|
||||||
|
},
|
||||||
|
'top'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async function showSummary (doc: Candidate): Promise<void> {
|
||||||
|
showPopup(IndexedDocumentPreview, { objectId: doc._id }, 'top')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
label={recruit.string.VacancyMatching}
|
||||||
|
okLabel={presentation.string.Ok}
|
||||||
|
on:close
|
||||||
|
okAction={() => {}}
|
||||||
|
canSave={true}
|
||||||
|
>
|
||||||
|
<Scroller horizontal>
|
||||||
|
<div
|
||||||
|
class="flex-row flex-nowrap"
|
||||||
|
use:resizeObserver={() => {
|
||||||
|
dispatch('changeContent')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex-row-center antiEmphasized">
|
||||||
|
<div class="p-1 flex-grow">
|
||||||
|
<SpaceSelect
|
||||||
|
size={'large'}
|
||||||
|
_class={recruit.class.Vacancy}
|
||||||
|
spaceQuery={{ archived: false }}
|
||||||
|
spaceOptions={orgOptions}
|
||||||
|
label={recruit.string.Vacancy}
|
||||||
|
create={{
|
||||||
|
component: recruit.component.CreateVacancy,
|
||||||
|
label: recruit.string.CreateVacancy
|
||||||
|
}}
|
||||||
|
bind:value={_space}
|
||||||
|
on:change={(evt) => {
|
||||||
|
_space = evt.detail
|
||||||
|
}}
|
||||||
|
component={VacancyOrgPresenter}
|
||||||
|
componentProps={{ inline: true }}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="content">
|
||||||
|
<VacancyCard {vacancy} disabled={true} />
|
||||||
|
</svelte:fragment>
|
||||||
|
</SpaceSelect>
|
||||||
|
</div>
|
||||||
|
<div class="p-1">
|
||||||
|
{#if vacancy}
|
||||||
|
<Scroller>
|
||||||
|
<div class="flex-col max-h-60">
|
||||||
|
{#if vacancy.description}
|
||||||
|
{vacancy.description}
|
||||||
|
{/if}
|
||||||
|
{#if vacancyState?.fullSummary}
|
||||||
|
<MessageViewer message={vacancyState?.fullSummary.split('\n').join('<br/>')} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Scroller>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Scroller>
|
||||||
|
<table class="antiTable mt-2">
|
||||||
|
<thead class="scroller-thead">
|
||||||
|
<tr class="scroller-thead__tr">
|
||||||
|
<td><Label label={recruit.string.Talent} /></td>
|
||||||
|
<td><Label label={recruit.string.Score} /></td>
|
||||||
|
<td><Label label={recruit.string.Match} /></td>
|
||||||
|
<td>#</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{#each _objects as doc}
|
||||||
|
{@const docState = state.get(doc._id)}
|
||||||
|
{@const docEmbedding = docState && getEmbedding(docState)}
|
||||||
|
{@const match = matches.get(doc._id)}
|
||||||
|
{@const appl = applications.get(doc._id)}
|
||||||
|
<tr class="antiTable-body__row">
|
||||||
|
<td>
|
||||||
|
<div class="flex-row-center">
|
||||||
|
<ObjectPresenter objectId={doc._id} _class={doc._class} value={doc} />
|
||||||
|
{#if appl}
|
||||||
|
<div class="ml-2 flex-row-center">
|
||||||
|
<ObjectPresenter objectId={appl._id} _class={appl._class} value={appl} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{#if docEmbedding && vacancyEmbedding}
|
||||||
|
{Math.round(cosinesim(docEmbedding, vacancyEmbedding) * 100)}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{#if match?.complete}
|
||||||
|
<div
|
||||||
|
class="lines-limit-4"
|
||||||
|
use:tooltip={{ component: MarkupPreviewPopup, props: { value: match.response } }}
|
||||||
|
>
|
||||||
|
<MessageViewer message={match.response} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
<td class="flex-row-center gap-2">
|
||||||
|
{#if docState}
|
||||||
|
<Button
|
||||||
|
label={recruit.string.PerformMatch}
|
||||||
|
loading={matching.has(doc._id) || !(match?.complete ?? true)}
|
||||||
|
on:click={() => requestMatch(doc, docState)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={IconActivity}
|
||||||
|
showTooltip={{ label: presentation.string.DocumentPreview }}
|
||||||
|
on:click={() => showSummary(doc)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={IconAdd}
|
||||||
|
disabled={appl !== undefined}
|
||||||
|
showTooltip={{ label: recruit.string.CreateVacancy }}
|
||||||
|
on:click={() => createApplication(doc, match)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Scroller>
|
||||||
|
</div>
|
||||||
|
</Scroller>
|
||||||
|
</Card>
|
@ -79,6 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if vacancy}
|
{#if vacancy}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div
|
<div
|
||||||
class="name lines-limit-2"
|
class="name lines-limit-2"
|
||||||
class:over-underline={!disabled}
|
class:over-underline={!disabled}
|
||||||
@ -95,22 +96,24 @@
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if inline}
|
<div class="text-md">
|
||||||
<div class="flex-row-center">
|
{#if inline}
|
||||||
<VacancyIcon size={'small'} />
|
<div class="flex-row-center">
|
||||||
<span class="ml-1">
|
<VacancyIcon size={'small'} />
|
||||||
{vacancy.name}
|
<span class="ml-1">
|
||||||
</span>
|
{vacancy.name}
|
||||||
</div>
|
</span>
|
||||||
{:else}
|
</div>
|
||||||
{vacancy.name}
|
{:else}
|
||||||
{/if}
|
{vacancy.name}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if company}
|
{#if company}
|
||||||
<span class="label">{company.name}</span>
|
<span class="label">{company.name}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !inline || vacancy.description}
|
{#if !inline || vacancy.description}
|
||||||
<div class="description lines-limit-2">{vacancy.description ?? ''}</div>
|
<div class="description lines-limit-2 text-md">{vacancy.description ?? ''}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="footer flex flex-reverse flex-grow">
|
<div class="footer flex flex-reverse flex-grow">
|
||||||
|
@ -62,6 +62,7 @@ import recruit from './plugin'
|
|||||||
import { objectIdProvider, objectLinkProvider, getApplicationTitle } from './utils'
|
import { objectIdProvider, objectLinkProvider, getApplicationTitle } from './utils'
|
||||||
import VacancyList from './components/VacancyList.svelte'
|
import VacancyList from './components/VacancyList.svelte'
|
||||||
import VacancyTemplateEditor from './components/VacancyTemplateEditor.svelte'
|
import VacancyTemplateEditor from './components/VacancyTemplateEditor.svelte'
|
||||||
|
import MatchVacancy from './components/MatchVacancy.svelte'
|
||||||
|
|
||||||
async function createOpinion (object: Doc): Promise<void> {
|
async function createOpinion (object: Doc): Promise<void> {
|
||||||
showPopup(CreateOpinion, { space: object.space, review: object._id })
|
showPopup(CreateOpinion, { space: object.space, review: object._id })
|
||||||
@ -300,7 +301,9 @@ export default async (): Promise<Resources> => ({
|
|||||||
ApplicantFilter,
|
ApplicantFilter,
|
||||||
|
|
||||||
VacancyList,
|
VacancyList,
|
||||||
VacancyTemplateEditor
|
VacancyTemplateEditor,
|
||||||
|
|
||||||
|
MatchVacancy
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
ApplicationQuery: async (
|
ApplicationQuery: async (
|
||||||
|
@ -111,7 +111,13 @@ export default mergeIds(recruitId, recruit, {
|
|||||||
HasActiveApplicant: '' as IntlString,
|
HasActiveApplicant: '' as IntlString,
|
||||||
HasNoActiveApplicant: '' as IntlString,
|
HasNoActiveApplicant: '' as IntlString,
|
||||||
NoneApplications: '' as IntlString,
|
NoneApplications: '' as IntlString,
|
||||||
RelatedIssues: '' as IntlString
|
RelatedIssues: '' as IntlString,
|
||||||
|
|
||||||
|
MatchVacancy: '' as IntlString,
|
||||||
|
VacancyMatching: '' as IntlString,
|
||||||
|
Score: '' as IntlString,
|
||||||
|
Match: '' as IntlString,
|
||||||
|
PerformMatch: '' as IntlString
|
||||||
},
|
},
|
||||||
space: {
|
space: {
|
||||||
CandidatesPublic: '' as Ref<Space>
|
CandidatesPublic: '' as Ref<Space>
|
||||||
|
@ -89,6 +89,18 @@ export interface Applicant extends Task {
|
|||||||
comments?: number
|
comments?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface ApplicantMatch extends AttachedDoc {
|
||||||
|
attachedTo: Ref<Candidate>
|
||||||
|
|
||||||
|
complete: boolean
|
||||||
|
vacancy: string
|
||||||
|
summary: string
|
||||||
|
response: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -129,6 +141,7 @@ const recruit = plugin(recruitId, {
|
|||||||
},
|
},
|
||||||
class: {
|
class: {
|
||||||
Applicant: '' as Ref<Class<Applicant>>,
|
Applicant: '' as Ref<Class<Applicant>>,
|
||||||
|
ApplicantMatch: '' as Ref<Class<ApplicantMatch>>,
|
||||||
Candidates: '' as Ref<Class<Candidates>>,
|
Candidates: '' as Ref<Class<Candidates>>,
|
||||||
Vacancy: '' as Ref<Class<Vacancy>>,
|
Vacancy: '' as Ref<Class<Vacancy>>,
|
||||||
Review: '' as Ref<Class<Review>>,
|
Review: '' as Ref<Class<Review>>,
|
||||||
|
@ -61,6 +61,7 @@
|
|||||||
"FilteredViews": "Filtered views",
|
"FilteredViews": "Filtered views",
|
||||||
"NewFilteredView": "New filtered view",
|
"NewFilteredView": "New filtered view",
|
||||||
"FilteredViewName": "Filtered view name",
|
"FilteredViewName": "Filtered view name",
|
||||||
"Than": "Than"
|
"Than": "Than",
|
||||||
|
"ShowPreviewOnClick": "Please click to show document index preview..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@
|
|||||||
"FilteredViews": "Фильтрованные отображения",
|
"FilteredViews": "Фильтрованные отображения",
|
||||||
"NewFilteredView": "Новое фильтрованное отображение",
|
"NewFilteredView": "Новое фильтрованное отображение",
|
||||||
"FilteredViewName": "Имя фильтрованного отображения",
|
"FilteredViewName": "Имя фильтрованного отображения",
|
||||||
"Than": "Затем"
|
"Than": "Затем",
|
||||||
|
"ShowPreviewOnClick": "Пожалуйста нажмите чтобы увидеть предпросмотр..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
<!--
|
||||||
|
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||||
|
// 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">
|
||||||
|
import { MessageViewer } from '@hcengineering/presentation'
|
||||||
|
import { deviceOptionsStore as deviceInfo, resizeObserver } from '@hcengineering/ui'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
|
export let value: string
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="antiCard {$deviceInfo.isMobile ? 'mobile' : 'dialog'}"
|
||||||
|
use:resizeObserver={() => {
|
||||||
|
dispatch('changeContent')
|
||||||
|
}}
|
||||||
|
on:close={() => dispatch('close', null)}
|
||||||
|
>
|
||||||
|
<div class="flex-grow mt-4">
|
||||||
|
<MessageViewer message={value} />
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -46,6 +46,8 @@
|
|||||||
export let tableId: string | undefined = undefined
|
export let tableId: string | undefined = undefined
|
||||||
export let readonly = false
|
export let readonly = false
|
||||||
|
|
||||||
|
export let prefferedSorting: string = 'modifiedOn'
|
||||||
|
|
||||||
// If defined, will show a number of dummy items before real data will appear.
|
// If defined, will show a number of dummy items before real data will appear.
|
||||||
export let loadingProps: LoadingProps | undefined = undefined
|
export let loadingProps: LoadingProps | undefined = undefined
|
||||||
|
|
||||||
@ -57,10 +59,16 @@
|
|||||||
|
|
||||||
$: lookup = options?.lookup ?? buildConfigLookup(hierarchy, _class, config)
|
$: lookup = options?.lookup ?? buildConfigLookup(hierarchy, _class, config)
|
||||||
|
|
||||||
let sortKey = 'modifiedOn'
|
let _sortKey = prefferedSorting
|
||||||
|
$: if (!userSorting) {
|
||||||
|
_sortKey = prefferedSorting
|
||||||
|
}
|
||||||
|
|
||||||
let sortOrder = SortingOrder.Descending
|
let sortOrder = SortingOrder.Descending
|
||||||
let loading = 0
|
let loading = 0
|
||||||
|
|
||||||
|
let userSorting = false
|
||||||
|
|
||||||
let objects: Doc[] = []
|
let objects: Doc[] = []
|
||||||
let objectsRecieved = false
|
let objectsRecieved = false
|
||||||
const refs: HTMLElement[] = []
|
const refs: HTMLElement[] = []
|
||||||
@ -71,7 +79,7 @@
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
$: sortingFunction = (config.find((it) => typeof it !== 'string' && it.sortingKey === sortKey) as BuildModelKey)
|
$: sortingFunction = (config.find((it) => typeof it !== 'string' && it.sortingKey === _sortKey) as BuildModelKey)
|
||||||
?.sortingFunction
|
?.sortingFunction
|
||||||
|
|
||||||
async function update (
|
async function update (
|
||||||
@ -107,7 +115,7 @@
|
|||||||
objects = []
|
objects = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$: update(_class, query, sortKey, sortOrder, lookup, options)
|
$: update(_class, query, _sortKey, sortOrder, lookup, options)
|
||||||
|
|
||||||
const showMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => {
|
const showMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => {
|
||||||
selection = row
|
selection = row
|
||||||
@ -121,12 +129,13 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSorting (key: string): void {
|
function changeSorting (key: string | string[]): void {
|
||||||
if (key === '') {
|
if (key === '') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (key !== sortKey) {
|
userSorting = true
|
||||||
sortKey = key
|
if (key !== _sortKey) {
|
||||||
|
_sortKey = Array.isArray(key) ? key[0] : key
|
||||||
sortOrder = SortingOrder.Ascending
|
sortOrder = SortingOrder.Ascending
|
||||||
} else {
|
} else {
|
||||||
sortOrder = sortOrder === SortingOrder.Ascending ? SortingOrder.Descending : SortingOrder.Ascending
|
sortOrder = sortOrder === SortingOrder.Ascending ? SortingOrder.Descending : SortingOrder.Ascending
|
||||||
@ -211,14 +220,14 @@
|
|||||||
{#each model as attribute}
|
{#each model as attribute}
|
||||||
<th
|
<th
|
||||||
class:sortable={attribute.sortingKey}
|
class:sortable={attribute.sortingKey}
|
||||||
class:sorted={attribute.sortingKey === sortKey}
|
class:sorted={attribute.sortingKey === _sortKey}
|
||||||
on:click={() => changeSorting(attribute.sortingKey)}
|
on:click={() => changeSorting(attribute.sortingKey)}
|
||||||
>
|
>
|
||||||
<div class="antiTable-cells">
|
<div class="antiTable-cells">
|
||||||
{#if attribute.label}
|
{#if attribute.label}
|
||||||
<Label label={attribute.label} />
|
<Label label={attribute.label} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if attribute.sortingKey === sortKey}
|
{#if attribute.sortingKey === _sortKey}
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
{#if sortOrder === SortingOrder.Ascending}
|
{#if sortOrder === SortingOrder.Ascending}
|
||||||
<IconUp size={'small'} />
|
<IconUp size={'small'} />
|
||||||
|
@ -14,12 +14,14 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
|
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
|
||||||
|
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||||
import { Scroller, tableSP } from '@hcengineering/ui'
|
import { Scroller, tableSP } from '@hcengineering/ui'
|
||||||
import { BuildModelKey } from '@hcengineering/view'
|
import { BuildModelKey } from '@hcengineering/view'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { ActionContext } from '..'
|
import { ActionContext } from '..'
|
||||||
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '../selection'
|
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '../selection'
|
||||||
import { LoadingProps } from '../utils'
|
import { LoadingProps } from '../utils'
|
||||||
|
import SourcePresenter from './inference/SourcePresenter.svelte'
|
||||||
import Table from './Table.svelte'
|
import Table from './Table.svelte'
|
||||||
|
|
||||||
export let _class: Ref<Class<Doc>>
|
export let _class: Ref<Class<Doc>>
|
||||||
@ -44,6 +46,32 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
;(document.activeElement as HTMLElement)?.blur()
|
;(document.activeElement as HTMLElement)?.blur()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Search config
|
||||||
|
let _config = config
|
||||||
|
|
||||||
|
let prefferedSorting: string = 'modifiedOn'
|
||||||
|
|
||||||
|
function updateConfig (config: (BuildModelKey | string)[], search?: string): void {
|
||||||
|
const useSearch = search !== '' && search != null
|
||||||
|
_config = [
|
||||||
|
...(useSearch
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: '',
|
||||||
|
presenter: SourcePresenter,
|
||||||
|
label: getEmbeddedLabel('#'),
|
||||||
|
sortingKey: '#score',
|
||||||
|
props: { search }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...config
|
||||||
|
]
|
||||||
|
prefferedSorting = !useSearch ? 'modifiedOn' : '#score'
|
||||||
|
}
|
||||||
|
|
||||||
|
$: updateConfig(config, query.$search)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window />
|
<svelte:window />
|
||||||
@ -57,7 +85,7 @@
|
|||||||
<Table
|
<Table
|
||||||
bind:this={table}
|
bind:this={table}
|
||||||
{_class}
|
{_class}
|
||||||
{config}
|
config={_config}
|
||||||
{options}
|
{options}
|
||||||
{query}
|
{query}
|
||||||
{showNotification}
|
{showNotification}
|
||||||
@ -66,6 +94,7 @@
|
|||||||
highlightRows={true}
|
highlightRows={true}
|
||||||
enableChecking
|
enableChecking
|
||||||
checked={$selectionStore ?? []}
|
checked={$selectionStore ?? []}
|
||||||
|
{prefferedSorting}
|
||||||
selection={listProvider.current($focusStore)}
|
selection={listProvider.current($focusStore)}
|
||||||
on:row-focus={(evt) => {
|
on:row-focus={(evt) => {
|
||||||
listProvider.updateFocus(evt.detail)
|
listProvider.updateFocus(evt.detail)
|
||||||
|
@ -15,7 +15,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Doc, WithLookup } from '@hcengineering/core'
|
import { Doc, WithLookup } from '@hcengineering/core'
|
||||||
import { IndexedDocumentPreview } from '@hcengineering/presentation'
|
import { IndexedDocumentPreview } from '@hcengineering/presentation'
|
||||||
import { showPopup } from '@hcengineering/ui'
|
import { showPopup, tooltip } from '@hcengineering/ui'
|
||||||
|
import plugin from '../../plugin'
|
||||||
|
|
||||||
export let value: WithLookup<Doc>
|
export let value: WithLookup<Doc>
|
||||||
export let search: string
|
export let search: string
|
||||||
@ -23,7 +24,14 @@
|
|||||||
|
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<span
|
<span
|
||||||
|
use:tooltip={{ label: plugin.string.ShowPreviewOnClick }}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
showPopup(IndexedDocumentPreview, { objectId: value._id, search })
|
showPopup(IndexedDocumentPreview, { objectId: value._id, search })
|
||||||
}}>{value.$source?.$score}</span
|
}}
|
||||||
>
|
>
|
||||||
|
{#if value.$source?.$score}
|
||||||
|
{Math.round(value.$source?.$score * 100) / 100}
|
||||||
|
{:else}
|
||||||
|
*
|
||||||
|
{/if}
|
||||||
|
</span>
|
@ -31,14 +31,23 @@ import EditBoxPopup from './components/EditBoxPopup.svelte'
|
|||||||
import EditDoc from './components/EditDoc.svelte'
|
import EditDoc from './components/EditDoc.svelte'
|
||||||
import EnumEditor from './components/EnumEditor.svelte'
|
import EnumEditor from './components/EnumEditor.svelte'
|
||||||
import FilterBar from './components/filter/FilterBar.svelte'
|
import FilterBar from './components/filter/FilterBar.svelte'
|
||||||
|
import FilterTypePopup from './components/filter/FilterTypePopup.svelte'
|
||||||
import ObjectFilter from './components/filter/ObjectFilter.svelte'
|
import ObjectFilter from './components/filter/ObjectFilter.svelte'
|
||||||
import TimestampFilter from './components/filter/TimestampFilter.svelte'
|
import TimestampFilter from './components/filter/TimestampFilter.svelte'
|
||||||
import ValueFilter from './components/filter/ValueFilter.svelte'
|
import ValueFilter from './components/filter/ValueFilter.svelte'
|
||||||
import FilterTypePopup from './components/filter/FilterTypePopup.svelte'
|
import HTMLEditor from './components/HTMLEditor.svelte'
|
||||||
import HTMLPresenter from './components/HTMLPresenter.svelte'
|
import HTMLPresenter from './components/HTMLPresenter.svelte'
|
||||||
|
import HyperlinkPresenter from './components/HyperlinkPresenter.svelte'
|
||||||
import IntlStringPresenter from './components/IntlStringPresenter.svelte'
|
import IntlStringPresenter from './components/IntlStringPresenter.svelte'
|
||||||
import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte'
|
import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte'
|
||||||
import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelte'
|
import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelte'
|
||||||
|
import GrowPresenter from './components/list/GrowPresenter.svelte'
|
||||||
|
import ListView from './components/list/ListView.svelte'
|
||||||
|
import SortableList from './components/list/SortableList.svelte'
|
||||||
|
import SortableListItem from './components/list/SortableListItem.svelte'
|
||||||
|
import MarkupEditor from './components/MarkupEditor.svelte'
|
||||||
|
import MarkupEditorPopup from './components/MarkupEditorPopup.svelte'
|
||||||
|
import MarkupPresenter from './components/MarkupPresenter.svelte'
|
||||||
import Menu from './components/Menu.svelte'
|
import Menu from './components/Menu.svelte'
|
||||||
import NumberEditor from './components/NumberEditor.svelte'
|
import NumberEditor from './components/NumberEditor.svelte'
|
||||||
import NumberPresenter from './components/NumberPresenter.svelte'
|
import NumberPresenter from './components/NumberPresenter.svelte'
|
||||||
@ -47,31 +56,22 @@ import RolePresenter from './components/RolePresenter.svelte'
|
|||||||
import SpacePresenter from './components/SpacePresenter.svelte'
|
import SpacePresenter from './components/SpacePresenter.svelte'
|
||||||
import StringEditor from './components/StringEditor.svelte'
|
import StringEditor from './components/StringEditor.svelte'
|
||||||
import StringPresenter from './components/StringPresenter.svelte'
|
import StringPresenter from './components/StringPresenter.svelte'
|
||||||
import HyperlinkPresenter from './components/HyperlinkPresenter.svelte'
|
|
||||||
import Table from './components/Table.svelte'
|
import Table from './components/Table.svelte'
|
||||||
import TableBrowser from './components/TableBrowser.svelte'
|
import TableBrowser from './components/TableBrowser.svelte'
|
||||||
import TimestampPresenter from './components/TimestampPresenter.svelte'
|
import TimestampPresenter from './components/TimestampPresenter.svelte'
|
||||||
import UpDownNavigator from './components/UpDownNavigator.svelte'
|
import UpDownNavigator from './components/UpDownNavigator.svelte'
|
||||||
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
|
||||||
import ValueSelector from './components/ValueSelector.svelte'
|
import ValueSelector from './components/ValueSelector.svelte'
|
||||||
import HTMLEditor from './components/HTMLEditor.svelte'
|
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
||||||
import MarkupPresenter from './components/MarkupPresenter.svelte'
|
|
||||||
import MarkupEditor from './components/MarkupEditor.svelte'
|
|
||||||
import MarkupEditorPopup from './components/MarkupEditorPopup.svelte'
|
|
||||||
import SortableList from './components/list/SortableList.svelte'
|
|
||||||
import SortableListItem from './components/list/SortableListItem.svelte'
|
|
||||||
import ListView from './components/list/ListView.svelte'
|
|
||||||
import GrowPresenter from './components/list/GrowPresenter.svelte'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
afterResult,
|
afterResult,
|
||||||
beforeResult,
|
beforeResult,
|
||||||
|
nestedDontMatchResult,
|
||||||
|
nestedMatchResult,
|
||||||
objectInResult,
|
objectInResult,
|
||||||
objectNinResult,
|
objectNinResult,
|
||||||
valueInResult,
|
valueInResult,
|
||||||
valueNinResult,
|
valueNinResult
|
||||||
nestedMatchResult,
|
|
||||||
nestedDontMatchResult
|
|
||||||
} from './filter'
|
} from './filter'
|
||||||
|
|
||||||
import { IndexedDocumentPreview } from '@hcengineering/presentation'
|
import { IndexedDocumentPreview } from '@hcengineering/presentation'
|
||||||
@ -84,30 +84,31 @@ export { getActions, invokeAction } from './actions'
|
|||||||
export { default as ActionContext } from './components/ActionContext.svelte'
|
export { default as ActionContext } from './components/ActionContext.svelte'
|
||||||
export { default as ActionHandler } from './components/ActionHandler.svelte'
|
export { default as ActionHandler } from './components/ActionHandler.svelte'
|
||||||
export { default as FilterButton } from './components/filter/FilterButton.svelte'
|
export { default as FilterButton } from './components/filter/FilterButton.svelte'
|
||||||
export { default as LinkPresenter } from './components/LinkPresenter.svelte'
|
|
||||||
export { default as ContextMenu } from './components/Menu.svelte'
|
|
||||||
export { default as TableBrowser } from './components/TableBrowser.svelte'
|
|
||||||
export { default as FixedColumn } from './components/FixedColumn.svelte'
|
export { default as FixedColumn } from './components/FixedColumn.svelte'
|
||||||
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
export { default as SourcePresenter } from './components/inference/SourcePresenter.svelte'
|
||||||
|
export { default as LinkPresenter } from './components/LinkPresenter.svelte'
|
||||||
|
export { default as List } from './components/list/List.svelte'
|
||||||
|
export { default as ContextMenu } from './components/Menu.svelte'
|
||||||
export { default as ObjectBox } from './components/ObjectBox.svelte'
|
export { default as ObjectBox } from './components/ObjectBox.svelte'
|
||||||
export { default as ObjectPresenter } from './components/ObjectPresenter.svelte'
|
export { default as ObjectPresenter } from './components/ObjectPresenter.svelte'
|
||||||
|
export { default as TableBrowser } from './components/TableBrowser.svelte'
|
||||||
export { default as List } from './components/list/List.svelte'
|
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
||||||
|
export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte'
|
||||||
export * from './context'
|
export * from './context'
|
||||||
export * from './filter'
|
export * from './filter'
|
||||||
export * from './selection'
|
export * from './selection'
|
||||||
export * from './viewOptions'
|
|
||||||
export {
|
export {
|
||||||
buildModel,
|
buildModel,
|
||||||
|
getActiveViewletId,
|
||||||
getCollectionCounter,
|
getCollectionCounter,
|
||||||
|
getFiltredKeys,
|
||||||
getObjectPresenter,
|
getObjectPresenter,
|
||||||
getObjectPreview,
|
getObjectPreview,
|
||||||
|
isCollectionAttr,
|
||||||
LoadingProps,
|
LoadingProps,
|
||||||
setActiveViewletId,
|
setActiveViewletId
|
||||||
getActiveViewletId,
|
|
||||||
getFiltredKeys,
|
|
||||||
isCollectionAttr
|
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
export * from './viewOptions'
|
||||||
export {
|
export {
|
||||||
HTMLPresenter,
|
HTMLPresenter,
|
||||||
Table,
|
Table,
|
||||||
|
33
plugins/view-resources/src/inference.ts
Normal file
33
plugins/view-resources/src/inference.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import core, { Class, Client, Doc, DocIndexState, Ref } from '@hcengineering/core'
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
|
export interface InferenceFocus {
|
||||||
|
objectId: Ref<Doc>
|
||||||
|
objectClass: Ref<Class<Doc>>
|
||||||
|
|
||||||
|
state: DocIndexState
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const inferenceFocusStore = writable<InferenceFocus | undefined>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export async function updateFocus (client: Client, doc?: { _id: Ref<Doc>, _class: Ref<Class<Doc>> }): Promise<void> {
|
||||||
|
if (doc === undefined) {
|
||||||
|
inferenceFocusStore.set(undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await client.findOne(core.class.DocIndexState, {
|
||||||
|
_id: doc._id as Ref<DocIndexState>,
|
||||||
|
objectClass: doc._class
|
||||||
|
})
|
||||||
|
if (state !== undefined) {
|
||||||
|
inferenceFocusStore.update(() => {
|
||||||
|
return { objectId: doc._id, objectClass: doc._class, state }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -61,6 +61,7 @@ export default mergeIds(viewId, view, {
|
|||||||
Grouping: '' as IntlString,
|
Grouping: '' as IntlString,
|
||||||
Ordering: '' as IntlString,
|
Ordering: '' as IntlString,
|
||||||
Manual: '' as IntlString,
|
Manual: '' as IntlString,
|
||||||
Than: '' as IntlString
|
Than: '' as IntlString,
|
||||||
|
ShowPreviewOnClick: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -548,3 +548,22 @@ export function getKeyLabel<T extends Doc> (
|
|||||||
return attribute.label
|
return attribute.label
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
* Implemenation of cosice similarity
|
||||||
|
*/
|
||||||
|
export function cosinesim (A: number[], B: number[]): number {
|
||||||
|
let dotproduct = 0
|
||||||
|
let mA = 0
|
||||||
|
let mB = 0
|
||||||
|
for (let i = 0; i < A.length; i++) {
|
||||||
|
dotproduct += A[i] * B[i]
|
||||||
|
mA += A[i] * A[i]
|
||||||
|
mB += B[i] * B[i]
|
||||||
|
}
|
||||||
|
mA = Math.sqrt(mA)
|
||||||
|
mB = Math.sqrt(mB)
|
||||||
|
const similarity = dotproduct / (mA * mB) // here you needed extra brackets
|
||||||
|
return similarity
|
||||||
|
}
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
|
import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||||
import { Asset, getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
import { Asset, IntlString } from '@hcengineering/platform'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import {
|
import {
|
||||||
AnyComponent,
|
AnyComponent,
|
||||||
@ -36,7 +36,6 @@
|
|||||||
setActiveViewletId,
|
setActiveViewletId,
|
||||||
ViewletSettingButton
|
ViewletSettingButton
|
||||||
} from '@hcengineering/view-resources'
|
} from '@hcengineering/view-resources'
|
||||||
import SourcePresenter from './search/SourcePresenter.svelte'
|
|
||||||
|
|
||||||
export let _class: Ref<Class<Doc>>
|
export let _class: Ref<Class<Doc>>
|
||||||
export let space: Ref<Space> | undefined = undefined
|
export let space: Ref<Space> | undefined = undefined
|
||||||
@ -139,20 +138,7 @@
|
|||||||
_class,
|
_class,
|
||||||
space,
|
space,
|
||||||
options: viewlet.options,
|
options: viewlet.options,
|
||||||
config: [
|
config: preference?.config ?? viewlet.config,
|
||||||
...(search !== ''
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: '',
|
|
||||||
presenter: SourcePresenter,
|
|
||||||
label: getEmbeddedLabel('#'),
|
|
||||||
sortingKey: '#score',
|
|
||||||
props: { search }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(preference?.config ?? viewlet.config)
|
|
||||||
],
|
|
||||||
viewlet,
|
viewlet,
|
||||||
viewOptions,
|
viewOptions,
|
||||||
createItemDialog: createComponent,
|
createItemDialog: createComponent,
|
||||||
|
@ -98,45 +98,60 @@ import { trackerId } from '@hcengineering/tracker'
|
|||||||
import { viewId } from '@hcengineering/view'
|
import { viewId } from '@hcengineering/view'
|
||||||
import { workbenchId } from '@hcengineering/workbench'
|
import { workbenchId } from '@hcengineering/workbench'
|
||||||
|
|
||||||
addStringsLoader(loginId, async (lang: string) => await import(`@hcengineering/login-assets/lang/${lang}.json`))
|
import loginEng from '@hcengineering/login-assets/lang/en.json'
|
||||||
addStringsLoader(taskId, async (lang: string) => await import(`@hcengineering/task-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(viewId, async (lang: string) => await import(`@hcengineering/view-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(chunterId, async (lang: string) => await import(`@hcengineering/chunter-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(
|
|
||||||
attachmentId,
|
|
||||||
async (lang: string) => await import(`@hcengineering/attachment-assets/lang/${lang}.json`)
|
|
||||||
)
|
|
||||||
addStringsLoader(contactId, async (lang: string) => await import(`@hcengineering/contact-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(recruitId, async (lang: string) => await import(`@hcengineering/recruit-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(activityId, async (lang: string) => await import(`@hcengineering/activity-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(
|
|
||||||
automationId,
|
|
||||||
async (lang: string) => await import(`@hcengineering/automation-assets/lang/${lang}.json`)
|
|
||||||
)
|
|
||||||
addStringsLoader(settingId, async (lang: string) => await import(`@hcengineering/setting-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(telegramId, async (lang: string) => await import(`@hcengineering/telegram-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(leadId, async (lang: string) => await import(`@hcengineering/lead-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(gmailId, async (lang: string) => await import(`@hcengineering/gmail-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(workbenchId, async (lang: string) => await import(`@hcengineering/workbench-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(inventoryId, async (lang: string) => await import(`@hcengineering/inventory-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(templatesId, async (lang: string) => await import(`@hcengineering/templates-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(
|
|
||||||
notificationId,
|
|
||||||
async (lang: string) => await import(`@hcengineering/notification-assets/lang/${lang}.json`)
|
|
||||||
)
|
|
||||||
addStringsLoader(tagsId, async (lang: string) => await import(`@hcengineering/tags-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(calendarId, async (lang: string) => await import(`@hcengineering/calendar-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(trackerId, async (lang: string) => await import(`@hcengineering/tracker-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(boardId, async (lang: string) => await import(`@hcengineering/board-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(
|
|
||||||
preferenceId,
|
|
||||||
async (lang: string) => await import(`@hcengineering/preference-assets/lang/${lang}.json`)
|
|
||||||
)
|
|
||||||
addStringsLoader(hrId, async (lang: string) => await import(`@hcengineering/hr-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(documentId, async (lang: string) => await import(`@hcengineering/document-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(bitrixId, async (lang: string) => await import(`@hcengineering/bitrix-assets/lang/${lang}.json`))
|
|
||||||
addStringsLoader(requestId, async (lang: string) => await import(`@hcengineering/request-assets/lang/${lang}.json`))
|
|
||||||
|
|
||||||
|
import taskEn from '@hcengineering/task-assets/lang/en.json'
|
||||||
|
import viewEn from '@hcengineering/view-assets/lang/en.json'
|
||||||
|
import chunterEn from '@hcengineering/chunter-assets/lang/en.json'
|
||||||
|
import attachmentEn from '@hcengineering/attachment-assets/lang/en.json'
|
||||||
|
import contactEn from '@hcengineering/contact-assets/lang/en.json'
|
||||||
|
import recruitEn from '@hcengineering/recruit-assets/lang/en.json'
|
||||||
|
import activityEn from '@hcengineering/activity-assets/lang/en.json'
|
||||||
|
import automationEn from '@hcengineering/automation-assets/lang/en.json'
|
||||||
|
import settingEn from '@hcengineering/setting-assets/lang/en.json'
|
||||||
|
import telegramEn from '@hcengineering/telegram-assets/lang/en.json'
|
||||||
|
import leadEn from '@hcengineering/lead-assets/lang/en.json'
|
||||||
|
import gmailEn from '@hcengineering/gmail-assets/lang/en.json'
|
||||||
|
import workbenchEn from '@hcengineering/workbench-assets/lang/en.json'
|
||||||
|
import inventoryEn from '@hcengineering/inventory-assets/lang/en.json'
|
||||||
|
import templatesEn from '@hcengineering/templates-assets/lang/en.json'
|
||||||
|
import notificationEn from '@hcengineering/notification-assets/lang/en.json'
|
||||||
|
import tagsEn from '@hcengineering/tags-assets/lang/en.json'
|
||||||
|
import calendarEn from '@hcengineering/calendar-assets/lang/en.json'
|
||||||
|
import trackerEn from '@hcengineering/tracker-assets/lang/en.json'
|
||||||
|
import boardEn from '@hcengineering/board-assets/lang/en.json'
|
||||||
|
import preferenceEn from '@hcengineering/preference-assets/lang/en.json'
|
||||||
|
import hrEn from '@hcengineering/hr-assets/lang/en.json'
|
||||||
|
import documentEn from '@hcengineering/document-assets/lang/en.json'
|
||||||
|
import bitrixEn from '@hcengineering/bitrix-assets/lang/en.json'
|
||||||
|
import requestEn from '@hcengineering/request-assets/lang/en.json'
|
||||||
|
addStringsLoader(loginId, async (lang: string) => loginEng)
|
||||||
|
|
||||||
|
addStringsLoader(taskId, async (lang: string) => taskEn)
|
||||||
|
addStringsLoader(viewId, async (lang: string) => viewEn)
|
||||||
|
addStringsLoader(chunterId, async (lang: string) => chunterEn)
|
||||||
|
addStringsLoader(attachmentId, async (lang: string) => attachmentEn)
|
||||||
|
addStringsLoader(contactId, async (lang: string) => contactEn)
|
||||||
|
addStringsLoader(recruitId, async (lang: string) => recruitEn)
|
||||||
|
addStringsLoader(activityId, async (lang: string) => activityEn)
|
||||||
|
addStringsLoader(automationId, async (lang: string) => automationEn)
|
||||||
|
addStringsLoader(settingId, async (lang: string) => settingEn)
|
||||||
|
addStringsLoader(telegramId, async (lang: string) => telegramEn)
|
||||||
|
addStringsLoader(leadId, async (lang: string) => leadEn)
|
||||||
|
addStringsLoader(gmailId, async (lang: string) => gmailEn)
|
||||||
|
addStringsLoader(workbenchId, async (lang: string) => workbenchEn)
|
||||||
|
addStringsLoader(inventoryId, async (lang: string) => inventoryEn)
|
||||||
|
addStringsLoader(templatesId, async (lang: string) => templatesEn)
|
||||||
|
addStringsLoader(notificationId, async (lang: string) => notificationEn)
|
||||||
|
addStringsLoader(tagsId, async (lang: string) => tagsEn)
|
||||||
|
addStringsLoader(calendarId, async (lang: string) => calendarEn)
|
||||||
|
addStringsLoader(trackerId, async (lang: string) => trackerEn)
|
||||||
|
addStringsLoader(boardId, async (lang: string) => boardEn)
|
||||||
|
addStringsLoader(preferenceId, async (lang: string) => preferenceEn)
|
||||||
|
addStringsLoader(hrId, async (lang: string) => hrEn)
|
||||||
|
addStringsLoader(documentId, async (lang: string) => documentEn)
|
||||||
|
addStringsLoader(bitrixId, async (lang: string) => bitrixEn)
|
||||||
|
addStringsLoader(requestId, async (lang: string) => requestEn)
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
"@hcengineering/server-core": "^0.6.1",
|
"@hcengineering/server-core": "^0.6.1",
|
||||||
"@hcengineering/server": "^0.6.4",
|
"@hcengineering/server": "^0.6.4",
|
||||||
"@hcengineering/chunter": "^0.6.2",
|
"@hcengineering/chunter": "^0.6.2",
|
||||||
|
"@hcengineering/recruit": "^0.6.4",
|
||||||
"got": "^11.8.3",
|
"got": "^11.8.3",
|
||||||
"fast-equals": "^2.0.3",
|
"fast-equals": "^2.0.3",
|
||||||
"html-to-text": "^9.0.3"
|
"html-to-text": "^9.0.3"
|
||||||
|
@ -20,6 +20,7 @@ import core, {
|
|||||||
DocumentQuery,
|
DocumentQuery,
|
||||||
DocumentUpdate,
|
DocumentUpdate,
|
||||||
docUpdKey,
|
docUpdKey,
|
||||||
|
IndexStageState,
|
||||||
MeasureContext,
|
MeasureContext,
|
||||||
Ref,
|
Ref,
|
||||||
Storage,
|
Storage,
|
||||||
@ -36,6 +37,7 @@ import {
|
|||||||
FullTextPipelineStage,
|
FullTextPipelineStage,
|
||||||
IndexedDoc,
|
IndexedDoc,
|
||||||
isIndexingRequired,
|
isIndexingRequired,
|
||||||
|
loadIndexStageStage,
|
||||||
RateLimitter
|
RateLimitter
|
||||||
} from '@hcengineering/server-core'
|
} from '@hcengineering/server-core'
|
||||||
|
|
||||||
@ -47,7 +49,7 @@ import openaiPlugin, { openAIRatelimitter } from './plugin'
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export const openAIstage = 'emb-v3a'
|
export const openAIstage = 'emb-v5'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -76,34 +78,37 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
|||||||
field = 'openai_embedding'
|
field = 'openai_embedding'
|
||||||
field_enabled = '_use'
|
field_enabled = '_use'
|
||||||
|
|
||||||
summary_field = 'summary'
|
|
||||||
|
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
clearExcept?: string[] = undefined
|
clearExcept?: string[] = undefined
|
||||||
updateFields: DocUpdateHandler[] = []
|
updateFields: DocUpdateHandler[] = []
|
||||||
|
|
||||||
model = process.env.OPENAI_MODEL ?? 'text-embedding-ada-002'
|
copyToState = true
|
||||||
|
|
||||||
|
model = 'text-embedding-ada-002'
|
||||||
|
|
||||||
tokenLimit = 8191
|
tokenLimit = 8191
|
||||||
|
|
||||||
endpoint = process.env.OPENAI_HOST ?? 'https://api.openai.com/v1/embeddings'
|
endpoint = 'https://api.openai.com/v1/embeddings'
|
||||||
token = ''
|
token = ''
|
||||||
|
|
||||||
rate = 5
|
rate = 5
|
||||||
|
|
||||||
|
stageValue: boolean | string = true
|
||||||
|
|
||||||
limitter = new RateLimitter(() => ({ rate: this.rate }))
|
limitter = new RateLimitter(() => ({ rate: this.rate }))
|
||||||
|
|
||||||
|
indexState?: IndexStageState
|
||||||
|
|
||||||
async update (doc: DocIndexState, update: DocumentUpdate<DocIndexState>): Promise<void> {}
|
async update (doc: DocIndexState, update: DocumentUpdate<DocIndexState>): Promise<void> {}
|
||||||
|
|
||||||
constructor (readonly adapter: FullTextAdapter, readonly metrics: MeasureContext, readonly workspaceId: WorkspaceId) {}
|
constructor (readonly adapter: FullTextAdapter, readonly metrics: MeasureContext, readonly workspaceId: WorkspaceId) {}
|
||||||
|
|
||||||
updateSummary (summary: FullSummaryStage): void {
|
updateSummary (summary: FullSummaryStage): void {
|
||||||
summary.fieldFilter.push((attr, value) => {
|
summary.fieldFilter.push((attr, value) => {
|
||||||
if (
|
const tMarkup = attr.type._class === core.class.TypeMarkup
|
||||||
attr.type._class === core.class.TypeMarkup &&
|
const lowerCase = value.toLocaleLowerCase()
|
||||||
(value.toLocaleLowerCase().startsWith('gpt:') || value.toLocaleLowerCase().startsWith('gpt Answer:'))
|
if (tMarkup && (lowerCase.includes('gpt:') || lowerCase.includes('gpt Answer:'))) {
|
||||||
) {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@ -150,6 +155,15 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
this.enabled = false
|
this.enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
;[this.stageValue, this.indexState] = await loadIndexStageStage(storage, this.indexState, this.stageId, 'config', {
|
||||||
|
enabled: this.enabled,
|
||||||
|
endpoint: this.endpoint,
|
||||||
|
field: this.field,
|
||||||
|
mode: this.model,
|
||||||
|
copyToState: this.copyToState,
|
||||||
|
stripNewLines: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEmbedding (text: string): Promise<OpenAIEmbeddingResponse> {
|
async getEmbedding (text: string): Promise<OpenAIEmbeddingResponse> {
|
||||||
@ -232,17 +246,18 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (query.$search === undefined) return { docs: [], pass: true }
|
if (query.$search === undefined) return { docs: [], pass: true }
|
||||||
const embeddingData = await this.getEmbedding(query.$search)
|
const queryString = query.$search.replace('\n ', ' ')
|
||||||
|
const embeddingData = await this.getEmbedding(queryString)
|
||||||
const embedding = embeddingData.data[0].embedding
|
const embedding = embeddingData.data[0].embedding
|
||||||
console.log('search embedding', embedding)
|
console.log('search embedding', embedding)
|
||||||
const docs = await this.adapter.searchEmbedding(_classes, query, embedding, {
|
const docs = await this.adapter.searchEmbedding(_classes, query, embedding, {
|
||||||
size,
|
size,
|
||||||
from,
|
from,
|
||||||
minScore: 0,
|
minScore: -100,
|
||||||
embeddingBoost: 100,
|
embeddingBoost: 100,
|
||||||
field: this.field,
|
field: this.field,
|
||||||
field_enable: this.field_enabled,
|
field_enable: this.field_enabled,
|
||||||
fulltextBoost: 1
|
fulltextBoost: 10
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
docs,
|
docs,
|
||||||
@ -251,6 +266,9 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async collect (toIndex: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
async collect (toIndex: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
for (const doc of toIndex) {
|
for (const doc of toIndex) {
|
||||||
if (pipeline.cancelling) {
|
if (pipeline.cancelling) {
|
||||||
return
|
return
|
||||||
@ -274,13 +292,7 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
|||||||
|
|
||||||
// No need to index this class, mark embeddings as empty ones.
|
// No need to index this class, mark embeddings as empty ones.
|
||||||
if (!needIndex) {
|
if (!needIndex) {
|
||||||
await pipeline.update(doc._id, true, {})
|
await pipeline.update(doc._id, this.stageValue, {})
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.token === '') {
|
|
||||||
// No token, just do nothing.
|
|
||||||
await pipeline.update(doc._id, true, {})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,10 +300,11 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
|||||||
if (this.unauthorized) {
|
if (this.unauthorized) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const embeddingText = (doc.attributes[this.summary_field] as string) ?? ''
|
const embeddingText = doc.fullSummary ?? ''
|
||||||
|
|
||||||
if (embeddingText.length > this.treshold) {
|
if (embeddingText.length > this.treshold) {
|
||||||
const embeddText = embeddingText
|
// replace newlines, which can negatively affect performance. Based on OpenAI examples.
|
||||||
|
const embeddText = embeddingText.replace('\n ', ' ')
|
||||||
|
|
||||||
console.log('calculate embeddings:', doc.objectClass, doc._id)
|
console.log('calculate embeddings:', doc.objectClass, doc._id)
|
||||||
|
|
||||||
@ -322,7 +335,11 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
|||||||
[this.field]: embedding,
|
[this.field]: embedding,
|
||||||
[this.field_enabled]: true
|
[this.field_enabled]: true
|
||||||
})
|
})
|
||||||
;(update as any)[docUpdKey(this.field)] = embedding.length
|
if (this.copyToState) {
|
||||||
|
;(update as any)[docUpdKey(this.field)] = embedding
|
||||||
|
} else {
|
||||||
|
;(update as any)[docUpdKey(this.field)] = embedding.length
|
||||||
|
}
|
||||||
;(update as any)[docUpdKey(this.field_enabled)] = true
|
;(update as any)[docUpdKey(this.field_enabled)] = true
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@ -337,18 +354,15 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
|||||||
}
|
}
|
||||||
// Print error only first time, and update it in doc index
|
// Print error only first time, and update it in doc index
|
||||||
console.error(err)
|
console.error(err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to collect all fields and prepare embedding document.
|
await pipeline.update(doc._id, this.stageValue, update)
|
||||||
|
|
||||||
await pipeline.update(doc._id, true, update)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove (docs: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
async remove (docs: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
||||||
// will be handled by field processor
|
// will be handled by field processor
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await pipeline.update(doc._id, true, {})
|
await pipeline.update(doc._id, this.stageValue, {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,15 +24,79 @@ import core, {
|
|||||||
TxCollectionCUD,
|
TxCollectionCUD,
|
||||||
TxCreateDoc,
|
TxCreateDoc,
|
||||||
TxCUD,
|
TxCUD,
|
||||||
TxProcessor,
|
TxProcessor
|
||||||
TxUpdateDoc
|
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import type { TriggerControl } from '@hcengineering/server-core'
|
import type { TriggerControl } from '@hcengineering/server-core'
|
||||||
import got from 'got'
|
import got from 'got'
|
||||||
import { convert } from 'html-to-text'
|
import { convert } from 'html-to-text'
|
||||||
import { encode } from './encoder/encoder'
|
import { chunks, encode } from './encoder/encoder'
|
||||||
import openai, { openAIRatelimitter } from './plugin'
|
import openai, { OpenAIConfiguration, openAIRatelimitter } from './plugin'
|
||||||
|
import recruit, { ApplicantMatch } from '@hcengineering/recruit'
|
||||||
|
|
||||||
|
const model = 'text-davinci-003'
|
||||||
|
|
||||||
|
const defaultOptions = {
|
||||||
|
max_tokens: 4000,
|
||||||
|
temperature: 0.9,
|
||||||
|
top_p: 1,
|
||||||
|
n: 1,
|
||||||
|
stop: null as string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performCompletion (
|
||||||
|
prompt: string,
|
||||||
|
options: typeof defaultOptions,
|
||||||
|
config: OpenAIConfiguration
|
||||||
|
): Promise<any> {
|
||||||
|
const ep = config.endpoint + '/completions'
|
||||||
|
|
||||||
|
const chunkedPrompt = chunks(prompt, options.max_tokens - 250)[0]
|
||||||
|
const tokens = encode(chunkedPrompt).length
|
||||||
|
|
||||||
|
let response: any
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
response = await openAIRatelimitter.exec(
|
||||||
|
async () =>
|
||||||
|
await got
|
||||||
|
.post(ep, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${config.token}`
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
model,
|
||||||
|
prompt: chunkedPrompt,
|
||||||
|
max_tokens: options.max_tokens - tokens,
|
||||||
|
temperature: options.temperature,
|
||||||
|
top_p: options.top_p,
|
||||||
|
n: options.n,
|
||||||
|
stream: false,
|
||||||
|
logprobs: null,
|
||||||
|
stop: options.stop
|
||||||
|
},
|
||||||
|
timeout: 180000
|
||||||
|
})
|
||||||
|
.json()
|
||||||
|
)
|
||||||
|
break
|
||||||
|
} catch (e: any) {
|
||||||
|
const msg = (e.message as string) ?? ''
|
||||||
|
if (
|
||||||
|
msg.includes('Response code 429 (Too Many Requests)') ||
|
||||||
|
msg.includes('Response code 503 (Service Unavailable)')
|
||||||
|
) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 1000)
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
console.error(e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -43,156 +107,200 @@ export async function OnGPTRequest (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
|
|||||||
const cud: TxCUD<Doc> = actualTx as TxCUD<Doc>
|
const cud: TxCUD<Doc> = actualTx as TxCUD<Doc>
|
||||||
//
|
//
|
||||||
if (tc.hierarchy.isDerived(cud.objectClass, chunter.class.Comment)) {
|
if (tc.hierarchy.isDerived(cud.objectClass, chunter.class.Comment)) {
|
||||||
let msg = ''
|
return await handleComment(tx, tc)
|
||||||
//
|
}
|
||||||
if (actualTx._class === core.class.TxCreateDoc) {
|
if (tc.hierarchy.isDerived(cud.objectClass, recruit.class.ApplicantMatch)) {
|
||||||
msg = (cud as TxCreateDoc<Comment>).attributes.message
|
return await handleApplicantMatch(tx, tc)
|
||||||
} else if (actualTx._class === core.class.TxUpdateDoc) {
|
|
||||||
msg = (cud as TxUpdateDoc<Comment>).operations.message ?? ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = convert(msg, {
|
|
||||||
preserveNewlines: true,
|
|
||||||
selectors: [{ selector: 'img', format: 'skip' }]
|
|
||||||
})
|
|
||||||
|
|
||||||
if (text.toLocaleLowerCase().startsWith('gpt:')) {
|
|
||||||
const [config] = await tc.findAll(openai.class.OpenAIConfiguration, {})
|
|
||||||
|
|
||||||
if (config?.enabled ?? false) {
|
|
||||||
// Elanbed, we could complete.
|
|
||||||
|
|
||||||
const split = text.split('\n')
|
|
||||||
let prompt = split.slice(1).join('\n').trim()
|
|
||||||
|
|
||||||
// Do prompt modifications.
|
|
||||||
|
|
||||||
const matches: string[] = []
|
|
||||||
for (const m of prompt.matchAll(/\${(\w+)}/gm)) {
|
|
||||||
for (const mm of m.values()) {
|
|
||||||
if (!mm.startsWith('${')) {
|
|
||||||
matches.push(mm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentTx = tx as TxCollectionCUD<Doc, Comment>
|
|
||||||
|
|
||||||
const [indexedData] = await tc.findAll(core.class.DocIndexState, {
|
|
||||||
_id: parentTx.objectId as Ref<DocIndexState>
|
|
||||||
})
|
|
||||||
const [parentDoc] = await tc.findAll(parentTx.objectClass, { _id: parentTx.objectId as Ref<DocIndexState> })
|
|
||||||
if (matches.length > 0) {
|
|
||||||
if (indexedData !== undefined) {
|
|
||||||
// Fill values in prompt.
|
|
||||||
for (const m of matches) {
|
|
||||||
const val = indexedData.attributes[m] ?? (parentDoc as any)[m]
|
|
||||||
if (val !== undefined) {
|
|
||||||
prompt = prompt.replace(`\${${m}}`, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
max_tokens: 4000,
|
|
||||||
temperature: 0.9,
|
|
||||||
top_p: 1,
|
|
||||||
n: 1,
|
|
||||||
stop: null as string | null
|
|
||||||
}
|
|
||||||
const configLine = split[0].slice(4).split(',')
|
|
||||||
for (const cfg of configLine) {
|
|
||||||
const vals = cfg.trim().split('=')
|
|
||||||
if (vals.length === 2) {
|
|
||||||
switch (vals[0].trim()) {
|
|
||||||
case 'max_tokens':
|
|
||||||
options.max_tokens = parseInt(vals[1])
|
|
||||||
break
|
|
||||||
case 'temperature':
|
|
||||||
options.temperature = parseFloat(vals[1])
|
|
||||||
break
|
|
||||||
case 'top_p':
|
|
||||||
options.top_p = parseInt(vals[1])
|
|
||||||
break
|
|
||||||
case 'n':
|
|
||||||
options.n = parseInt(vals[1])
|
|
||||||
break
|
|
||||||
case 'stop':
|
|
||||||
options.stop = vals[1]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ep = config.endpoint + '/completions'
|
|
||||||
|
|
||||||
const tokens = encode(prompt).length
|
|
||||||
|
|
||||||
let response: any
|
|
||||||
try {
|
|
||||||
response = await openAIRatelimitter.exec(
|
|
||||||
async () =>
|
|
||||||
await got
|
|
||||||
.post(ep, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${config.token}`
|
|
||||||
},
|
|
||||||
json: {
|
|
||||||
model: 'text-davinci-003',
|
|
||||||
prompt,
|
|
||||||
max_tokens: options.max_tokens - tokens,
|
|
||||||
temperature: options.temperature,
|
|
||||||
top_p: options.top_p,
|
|
||||||
n: options.n,
|
|
||||||
stream: false,
|
|
||||||
logprobs: null,
|
|
||||||
stop: options.stop
|
|
||||||
},
|
|
||||||
timeout: 60000
|
|
||||||
})
|
|
||||||
.json()
|
|
||||||
)
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
console.log('response is good')
|
|
||||||
const result: Tx[] = []
|
|
||||||
|
|
||||||
for (const choices of response.choices) {
|
|
||||||
const msgTx = tc.txFactory.createTxCreateDoc(chunter.class.Comment, tx.objectSpace, {
|
|
||||||
message: 'gpt Answer:\n<br/>' + (choices.text as string).replace('\n', '\n<br/>'),
|
|
||||||
attachedTo: parentTx.objectId,
|
|
||||||
attachedToClass: parentTx.objectClass,
|
|
||||||
collection: parentTx.collection
|
|
||||||
})
|
|
||||||
// msgTx.modifiedBy = openai.account.GPT
|
|
||||||
const col = tc.txFactory.createTxCollectionCUD(
|
|
||||||
parentTx.objectClass,
|
|
||||||
parentTx.objectId,
|
|
||||||
parentTx.objectSpace,
|
|
||||||
parentTx.collection,
|
|
||||||
msgTx
|
|
||||||
)
|
|
||||||
// col.modifiedBy = openai.account.GPT
|
|
||||||
result.push(col)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store response transactions
|
|
||||||
await tc.txFx(async (st) => {
|
|
||||||
for (const t of result) {
|
|
||||||
await st.tx(t)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleComment (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
|
||||||
|
const actualTx = TxProcessor.extractTx(tx)
|
||||||
|
const cud: TxCUD<Doc> = actualTx as TxCUD<Doc>
|
||||||
|
|
||||||
|
let msg = ''
|
||||||
|
//
|
||||||
|
if (actualTx._class === core.class.TxCreateDoc) {
|
||||||
|
msg = (cud as TxCreateDoc<Comment>).attributes.message
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = convert(msg, {
|
||||||
|
preserveNewlines: true,
|
||||||
|
selectors: [{ selector: 'img', format: 'skip' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (text.toLocaleLowerCase().startsWith('gpt:')) {
|
||||||
|
const [config] = await tc.findAll(openai.class.OpenAIConfiguration, {})
|
||||||
|
|
||||||
|
if (config?.enabled ?? false) {
|
||||||
|
// Elanbed, we could complete.
|
||||||
|
|
||||||
|
const split = text.split('\n')
|
||||||
|
let prompt = split.slice(1).join('\n').trim()
|
||||||
|
|
||||||
|
// Do prompt modifications.
|
||||||
|
|
||||||
|
const matches: string[] = []
|
||||||
|
for (const m of prompt.matchAll(/\${(\w+)}/gm)) {
|
||||||
|
for (const mm of m.values()) {
|
||||||
|
if (!mm.startsWith('${')) {
|
||||||
|
matches.push(mm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentTx = tx as TxCollectionCUD<Doc, Comment>
|
||||||
|
|
||||||
|
const [indexedData] = await tc.findAll(core.class.DocIndexState, {
|
||||||
|
_id: parentTx.objectId as Ref<DocIndexState>
|
||||||
|
})
|
||||||
|
|
||||||
|
const [parentDoc] = await tc.findAll(parentTx.objectClass, { _id: parentTx.objectId as Ref<DocIndexState> })
|
||||||
|
const values: Record<string, any> = {
|
||||||
|
...indexedData.attributes,
|
||||||
|
...parentDoc,
|
||||||
|
summary: indexedData.fullSummary,
|
||||||
|
shortSummary: indexedData.shortSummary
|
||||||
|
}
|
||||||
|
if (matches.length > 0) {
|
||||||
|
if (indexedData !== undefined) {
|
||||||
|
// Fill values in prompt.
|
||||||
|
for (const m of matches) {
|
||||||
|
const val = values[m]
|
||||||
|
if (val !== undefined) {
|
||||||
|
prompt = prompt.replace(`\${${m}}`, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = parseOptions(split)
|
||||||
|
|
||||||
|
const response = await performCompletion(prompt, options, config)
|
||||||
|
const result: Tx[] = []
|
||||||
|
|
||||||
|
let finalMsg = msg + '</br>'
|
||||||
|
|
||||||
|
for (const choices of response.choices) {
|
||||||
|
const val = (choices.text as string).trim().split('\n').join('\n<br/>')
|
||||||
|
finalMsg += `<p>Answer:\n<br/>${val}</p>`
|
||||||
|
}
|
||||||
|
const msgTx = tc.txFactory.createTxUpdateDoc<Comment>(
|
||||||
|
cud.objectClass,
|
||||||
|
cud.objectSpace,
|
||||||
|
cud.objectId as Ref<Comment>,
|
||||||
|
{
|
||||||
|
message: finalMsg
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// msgTx.modifiedBy = openai.account.GPT
|
||||||
|
const col = tc.txFactory.createTxCollectionCUD(
|
||||||
|
parentTx.objectClass,
|
||||||
|
parentTx.objectId,
|
||||||
|
parentTx.objectSpace,
|
||||||
|
parentTx.collection,
|
||||||
|
msgTx
|
||||||
|
)
|
||||||
|
// col.modifiedBy = openai.account.GPT
|
||||||
|
result.push(col)
|
||||||
|
|
||||||
|
// Store response transactions
|
||||||
|
await tc.txFx(async (st) => {
|
||||||
|
for (const t of result) {
|
||||||
|
await st.tx(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
async function handleApplicantMatch (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
|
||||||
|
const [config] = await tc.findAll(openai.class.OpenAIConfiguration, {})
|
||||||
|
|
||||||
|
if (!(config?.enabled ?? false)) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const actualTx = TxProcessor.extractTx(tx)
|
||||||
|
const parentTx = tx as TxCollectionCUD<Doc, ApplicantMatch>
|
||||||
|
|
||||||
|
if (actualTx._class !== core.class.TxCreateDoc) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const cud: TxCreateDoc<ApplicantMatch> = actualTx as TxCreateDoc<ApplicantMatch>
|
||||||
|
|
||||||
|
const options: typeof defaultOptions = {
|
||||||
|
...defaultOptions,
|
||||||
|
temperature: 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxAnswerTokens = 500
|
||||||
|
const maxVacancyTokens = options.max_tokens - maxAnswerTokens / 2
|
||||||
|
const maxCandidateTokens = maxVacancyTokens
|
||||||
|
|
||||||
|
let candidateText = cud.attributes.summary
|
||||||
|
|
||||||
|
candidateText = convert(candidateText, {
|
||||||
|
preserveNewlines: true,
|
||||||
|
selectors: [{ selector: 'img', format: 'skip' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
candidateText = chunks(candidateText, maxCandidateTokens)[0]
|
||||||
|
|
||||||
|
let vacancyText = cud.attributes.vacancy
|
||||||
|
|
||||||
|
vacancyText = convert(vacancyText, {
|
||||||
|
preserveNewlines: true,
|
||||||
|
selectors: [{ selector: 'img', format: 'skip' }]
|
||||||
|
})
|
||||||
|
vacancyText = chunks(vacancyText, maxVacancyTokens)[0]
|
||||||
|
|
||||||
|
// Enabled, we could complete.
|
||||||
|
|
||||||
|
const text = `'Considering following vacancy:\n ${vacancyText}\n write if following candidate good for vacancy and why:\n ${candidateText}\n`
|
||||||
|
|
||||||
|
const response = await performCompletion(text, options, config)
|
||||||
|
const result: Tx[] = []
|
||||||
|
|
||||||
|
let finalMsg = ''
|
||||||
|
|
||||||
|
for (const choices of response.choices) {
|
||||||
|
let val = (choices.text as string).trim()
|
||||||
|
// Add new line before Reason:
|
||||||
|
val = val.split('\n\n').join('\n')
|
||||||
|
val = val.replace('Reason:', '\nReason:')
|
||||||
|
val = val.replace('Candidate is', '\nCandidate is')
|
||||||
|
val = val.replace(/Match score: (\d+\/\d+|\d+%) /gi, (val) => val + '\n')
|
||||||
|
|
||||||
|
val = val.split('\n').join('\n<br/>')
|
||||||
|
finalMsg += `<p>${val}</p>`
|
||||||
|
}
|
||||||
|
const msgTx = tc.txFactory.createTxUpdateDoc<ApplicantMatch>(cud.objectClass, cud.objectSpace, cud.objectId, {
|
||||||
|
response: finalMsg,
|
||||||
|
complete: true
|
||||||
|
})
|
||||||
|
// msgTx.modifiedBy = openai.account.GPT
|
||||||
|
const col = tc.txFactory.createTxCollectionCUD(
|
||||||
|
parentTx.objectClass,
|
||||||
|
parentTx.objectId,
|
||||||
|
parentTx.objectSpace,
|
||||||
|
parentTx.collection,
|
||||||
|
msgTx
|
||||||
|
)
|
||||||
|
// col.modifiedBy = openai.account.GPT
|
||||||
|
result.push(col)
|
||||||
|
|
||||||
|
// Store response transactions
|
||||||
|
await tc.txFx(async (st) => {
|
||||||
|
for (const t of result) {
|
||||||
|
await st.tx(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -201,3 +309,30 @@ export const openAIPluginImpl = async () => ({
|
|||||||
OnGPTRequest
|
OnGPTRequest
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
function parseOptions (split: string[]): typeof defaultOptions {
|
||||||
|
const options = defaultOptions
|
||||||
|
const configLine = split[0].slice(4).split(',')
|
||||||
|
for (const cfg of configLine) {
|
||||||
|
const vals = cfg.trim().split('=')
|
||||||
|
if (vals.length === 2) {
|
||||||
|
switch (vals[0].trim()) {
|
||||||
|
case 'max_tokens':
|
||||||
|
options.max_tokens = parseInt(vals[1])
|
||||||
|
break
|
||||||
|
case 'temperature':
|
||||||
|
options.temperature = parseFloat(vals[1])
|
||||||
|
break
|
||||||
|
case 'top_p':
|
||||||
|
options.top_p = parseInt(vals[1])
|
||||||
|
break
|
||||||
|
case 'n':
|
||||||
|
options.n = parseInt(vals[1])
|
||||||
|
break
|
||||||
|
case 'stop':
|
||||||
|
options.stop = vals[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
@ -48,6 +48,8 @@ export class ContentRetrievalStage implements FullTextPipelineStage {
|
|||||||
|
|
||||||
textLimit = 100 * 1024
|
textLimit = 100 * 1024
|
||||||
|
|
||||||
|
stageValue: boolean | string = true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
readonly storageAdapter: MinioService | undefined,
|
readonly storageAdapter: MinioService | undefined,
|
||||||
readonly workspace: WorkspaceId,
|
readonly workspace: WorkspaceId,
|
||||||
|
@ -50,6 +50,8 @@ export class IndexedFieldStage implements FullTextPipelineStage {
|
|||||||
|
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
|
stageValue: boolean | string = true
|
||||||
|
|
||||||
constructor (private readonly dbStorage: ServerStorage, readonly metrics: MeasureContext) {}
|
constructor (private readonly dbStorage: ServerStorage, readonly metrics: MeasureContext) {}
|
||||||
|
|
||||||
async initialize (storage: Storage, pipeline: FullTextPipeline): Promise<void> {
|
async initialize (storage: Storage, pipeline: FullTextPipeline): Promise<void> {
|
||||||
|
@ -54,6 +54,8 @@ export class FullTextPushStage implements FullTextPipelineStage {
|
|||||||
|
|
||||||
field_enabled = '_use'
|
field_enabled = '_use'
|
||||||
|
|
||||||
|
stageValue: boolean | string = true
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
readonly fulltextAdapter: FullTextAdapter,
|
readonly fulltextAdapter: FullTextAdapter,
|
||||||
readonly workspace: WorkspaceId,
|
readonly workspace: WorkspaceId,
|
||||||
|
@ -23,6 +23,7 @@ import core, {
|
|||||||
DOMAIN_DOC_INDEX_STATE,
|
DOMAIN_DOC_INDEX_STATE,
|
||||||
Hierarchy,
|
Hierarchy,
|
||||||
MeasureContext,
|
MeasureContext,
|
||||||
|
ModelDb,
|
||||||
Ref,
|
Ref,
|
||||||
ServerStorage,
|
ServerStorage,
|
||||||
setObjectValue,
|
setObjectValue,
|
||||||
@ -77,7 +78,8 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
private readonly stages: FullTextPipelineStage[],
|
private readonly stages: FullTextPipelineStage[],
|
||||||
readonly hierarchy: Hierarchy,
|
readonly hierarchy: Hierarchy,
|
||||||
readonly workspace: WorkspaceId,
|
readonly workspace: WorkspaceId,
|
||||||
readonly metrics: MeasureContext
|
readonly metrics: MeasureContext,
|
||||||
|
readonly model: ModelDb
|
||||||
) {
|
) {
|
||||||
this.readyStages = stages.map((it) => it.stageId)
|
this.readyStages = stages.map((it) => it.stageId)
|
||||||
this.readyStages.sort()
|
this.readyStages.sort()
|
||||||
@ -101,6 +103,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
): Promise<{ docs: IndexedDoc[], pass: boolean }> {
|
): Promise<{ docs: IndexedDoc[], pass: boolean }> {
|
||||||
const result: IndexedDoc[] = []
|
const result: IndexedDoc[] = []
|
||||||
for (const st of this.stages) {
|
for (const st of this.stages) {
|
||||||
|
await st.initialize(this.storage, this)
|
||||||
const docs = await st.search(_classes, search, size, from)
|
const docs = await st.search(_classes, search, size, from)
|
||||||
result.push(...docs.docs)
|
result.push(...docs.docs)
|
||||||
if (!docs.pass) {
|
if (!docs.pass) {
|
||||||
@ -154,7 +157,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
// Update are commulative
|
// Update are commulative
|
||||||
async update (
|
async update (
|
||||||
docId: Ref<DocIndexState>,
|
docId: Ref<DocIndexState>,
|
||||||
mark: boolean,
|
mark: boolean | string,
|
||||||
update: DocumentUpdate<DocIndexState>,
|
update: DocumentUpdate<DocIndexState>,
|
||||||
flush?: boolean
|
flush?: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -162,7 +165,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
if (udoc !== undefined) {
|
if (udoc !== undefined) {
|
||||||
await this.stageUpdate(udoc, update)
|
await this.stageUpdate(udoc, update)
|
||||||
|
|
||||||
udoc = this.updateDoc(udoc, update, mark)
|
udoc = this.updateDoc(udoc, update, mark !== false)
|
||||||
this.toIndex.set(docId, udoc)
|
this.toIndex.set(docId, udoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +173,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
udoc = this.toIndexParents.get(docId)
|
udoc = this.toIndexParents.get(docId)
|
||||||
if (udoc !== undefined) {
|
if (udoc !== undefined) {
|
||||||
await this.stageUpdate(udoc, update)
|
await this.stageUpdate(udoc, update)
|
||||||
udoc = this.updateDoc(udoc, update, mark)
|
udoc = this.updateDoc(udoc, update, mark !== false)
|
||||||
this.toIndexParents.set(docId, udoc)
|
this.toIndexParents.set(docId, udoc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -294,7 +297,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
const result = await this.storage.findAll(
|
const result = await this.storage.findAll(
|
||||||
core.class.DocIndexState,
|
core.class.DocIndexState,
|
||||||
{
|
{
|
||||||
[`stages.${st.stageId}`]: { $nin: [true] },
|
[`stages.${st.stageId}`]: { $nin: [st.stageValue] },
|
||||||
_id: { $nin: toSkip },
|
_id: { $nin: toSkip },
|
||||||
removed: false
|
removed: false
|
||||||
},
|
},
|
||||||
@ -346,7 +349,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
|
|
||||||
// Check items with not updated state.
|
// Check items with not updated state.
|
||||||
for (const d of toIndex) {
|
for (const d of toIndex) {
|
||||||
if (!d.stages?.[st.stageId]) {
|
if (d.stages?.[st.stageId] === false) {
|
||||||
this.skipped.set(d._id, (this.skipped.get(d._id) ?? 0) + 1)
|
this.skipped.set(d._id, (this.skipped.get(d._id) ?? 0) + 1)
|
||||||
} else {
|
} else {
|
||||||
this.skipped.delete(d._id)
|
this.skipped.delete(d._id)
|
||||||
@ -420,7 +423,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
}
|
}
|
||||||
for (const st of statistics) {
|
for (const st of statistics) {
|
||||||
for (const [s, v] of Object.entries(st.stages ?? {})) {
|
for (const [s, v] of Object.entries(st.stages ?? {})) {
|
||||||
if (v && allStageIds.has(s)) {
|
if (v !== false && allStageIds.has(s)) {
|
||||||
this.stats[s] = (this.stats[s] ?? 0) + 1
|
this.stats[s] = (this.stats[s] ?? 0) + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,23 +20,24 @@ import core, {
|
|||||||
DocIndexState,
|
DocIndexState,
|
||||||
DocumentQuery,
|
DocumentQuery,
|
||||||
DocumentUpdate,
|
DocumentUpdate,
|
||||||
docUpdKey,
|
|
||||||
extractDocKey,
|
extractDocKey,
|
||||||
FullTextSearchContext,
|
FullTextSearchContext,
|
||||||
Hierarchy,
|
Hierarchy,
|
||||||
|
IndexStageState,
|
||||||
isFullTextAttribute,
|
isFullTextAttribute,
|
||||||
Ref,
|
Ref,
|
||||||
Storage
|
Storage
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { translate } from '@hcengineering/platform'
|
import { translate } from '@hcengineering/platform'
|
||||||
|
import { convert } from 'html-to-text'
|
||||||
import { IndexedDoc } from '../types'
|
import { IndexedDoc } from '../types'
|
||||||
import { contentStageId, DocUpdateHandler, fieldStateId, FullTextPipeline, FullTextPipelineStage } from './types'
|
import { contentStageId, DocUpdateHandler, fieldStateId, FullTextPipeline, FullTextPipelineStage } from './types'
|
||||||
import { convert } from 'html-to-text'
|
import { loadIndexStageStage } from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export const summaryStageId = 'sum-v2'
|
export const summaryStageId = 'sum-v3a'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -54,11 +55,22 @@ export class FullSummaryStage implements FullTextPipelineStage {
|
|||||||
// If specified, index only fields with content speciffied.
|
// If specified, index only fields with content speciffied.
|
||||||
matchExtra: string[] = [] // 'content', 'base64'] // '#en'
|
matchExtra: string[] = [] // 'content', 'base64'] // '#en'
|
||||||
|
|
||||||
summaryField = 'summary'
|
|
||||||
|
|
||||||
fieldFilter: ((attr: AnyAttribute, value: string) => boolean)[] = []
|
fieldFilter: ((attr: AnyAttribute, value: string) => boolean)[] = []
|
||||||
|
|
||||||
async initialize (storage: Storage, pipeline: FullTextPipeline): Promise<void> {}
|
stageValue: boolean | string = true
|
||||||
|
|
||||||
|
indexState?: IndexStageState
|
||||||
|
|
||||||
|
async initialize (storage: Storage, pipeline: FullTextPipeline): Promise<void> {
|
||||||
|
const indexable = (
|
||||||
|
await pipeline.model.findAll(core.class.Class, { [core.mixin.FullTextSearchContext + '.fullTextSummary']: true })
|
||||||
|
).map((it) => it._id)
|
||||||
|
indexable.sort()
|
||||||
|
;[this.stageValue, this.indexState] = await loadIndexStageStage(storage, this.indexState, this.stageId, 'config', {
|
||||||
|
classes: indexable,
|
||||||
|
matchExtra: this.matchExtra
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async search (
|
async search (
|
||||||
_classes: Ref<Class<Doc>>[],
|
_classes: Ref<Class<Doc>>[],
|
||||||
@ -79,7 +91,7 @@ export class FullSummaryStage implements FullTextPipelineStage {
|
|||||||
|
|
||||||
// No need to index this class, mark embeddings as empty ones.
|
// No need to index this class, mark embeddings as empty ones.
|
||||||
if (!needIndex) {
|
if (!needIndex) {
|
||||||
await pipeline.update(doc._id, true, {})
|
await pipeline.update(doc._id, this.stageValue, {})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,16 +101,16 @@ export class FullSummaryStage implements FullTextPipelineStage {
|
|||||||
matchExtra: this.matchExtra,
|
matchExtra: this.matchExtra,
|
||||||
fieldFilter: this.fieldFilter
|
fieldFilter: this.fieldFilter
|
||||||
})
|
})
|
||||||
;(update as any)[docUpdKey(this.summaryField)] = embeddingText
|
update.fullSummary = embeddingText
|
||||||
|
|
||||||
await pipeline.update(doc._id, true, update)
|
await pipeline.update(doc._id, this.stageValue, update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove (docs: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
async remove (docs: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
||||||
// will be handled by field processor
|
// will be handled by field processor
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await pipeline.update(doc._id, true, {})
|
await pipeline.update(doc._id, this.stageValue, {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -185,6 +197,10 @@ export async function extractIndexedValues (
|
|||||||
if (!isFullTextAttribute(keyAttr)) {
|
if (!isFullTextAttribute(keyAttr)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (keyAttr.type._class === core.class.TypeAttachment && extra.length === 0) {
|
||||||
|
// Skipt attachment id values.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const repl = extra.join('#')
|
const repl = extra.join('#')
|
||||||
|
|
||||||
|
@ -13,7 +13,17 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import { Class, Doc, DocIndexState, DocumentQuery, DocumentUpdate, Hierarchy, Ref, Storage } from '@hcengineering/core'
|
import {
|
||||||
|
Class,
|
||||||
|
Doc,
|
||||||
|
DocIndexState,
|
||||||
|
DocumentQuery,
|
||||||
|
DocumentUpdate,
|
||||||
|
Hierarchy,
|
||||||
|
ModelDb,
|
||||||
|
Ref,
|
||||||
|
Storage
|
||||||
|
} from '@hcengineering/core'
|
||||||
import type { IndexedDoc } from '../types'
|
import type { IndexedDoc } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,9 +31,10 @@ import type { IndexedDoc } from '../types'
|
|||||||
*/
|
*/
|
||||||
export interface FullTextPipeline {
|
export interface FullTextPipeline {
|
||||||
hierarchy: Hierarchy
|
hierarchy: Hierarchy
|
||||||
|
model: ModelDb
|
||||||
update: (
|
update: (
|
||||||
docId: Ref<DocIndexState>,
|
docId: Ref<DocIndexState>,
|
||||||
mark: boolean,
|
mark: boolean | string,
|
||||||
update: DocumentUpdate<DocIndexState>,
|
update: DocumentUpdate<DocIndexState>,
|
||||||
flush?: boolean
|
flush?: boolean
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
@ -61,6 +72,8 @@ export interface FullTextPipelineStage {
|
|||||||
|
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|
||||||
|
stageValue: boolean | string
|
||||||
|
|
||||||
initialize: (storage: Storage, pipeline: FullTextPipeline) => Promise<void>
|
initialize: (storage: Storage, pipeline: FullTextPipeline) => Promise<void>
|
||||||
|
|
||||||
// Collect all changes related to bulk of document states
|
// Collect all changes related to bulk of document states
|
||||||
|
@ -28,12 +28,17 @@ import core, {
|
|||||||
DOMAIN_MODEL,
|
DOMAIN_MODEL,
|
||||||
DOMAIN_TRANSIENT,
|
DOMAIN_TRANSIENT,
|
||||||
DOMAIN_TX,
|
DOMAIN_TX,
|
||||||
|
generateId,
|
||||||
Hierarchy,
|
Hierarchy,
|
||||||
|
IndexStageState,
|
||||||
isFullTextAttribute,
|
isFullTextAttribute,
|
||||||
Obj,
|
Obj,
|
||||||
Ref,
|
Ref,
|
||||||
Space
|
Space,
|
||||||
|
Storage,
|
||||||
|
TxFactory
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
|
import { deepEqual } from 'fast-equals'
|
||||||
import plugin from '../plugin'
|
import plugin from '../plugin'
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@ -155,3 +160,52 @@ export function createStateDoc (
|
|||||||
...data
|
...data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export async function loadIndexStageStage (
|
||||||
|
storage: Storage,
|
||||||
|
state: IndexStageState | undefined,
|
||||||
|
stageId: string,
|
||||||
|
field: string,
|
||||||
|
newValue: any
|
||||||
|
): Promise<[boolean | string, IndexStageState]> {
|
||||||
|
if (state === undefined) {
|
||||||
|
;[state] = await storage.findAll(core.class.IndexStageState, { stageId })
|
||||||
|
}
|
||||||
|
const attributes: Record<string, any> = state?.attributes ?? {}
|
||||||
|
|
||||||
|
let result: boolean | string | undefined = attributes?.index !== undefined ? `${attributes?.index as number}` : true
|
||||||
|
|
||||||
|
if (!deepEqual(attributes[field], newValue)) {
|
||||||
|
// Not match,
|
||||||
|
const newIndex = ((attributes.index as number) ?? 0) + 1
|
||||||
|
result = `${newIndex}`
|
||||||
|
|
||||||
|
const ops = new TxFactory(core.account.System)
|
||||||
|
const data = {
|
||||||
|
stageId,
|
||||||
|
attributes: {
|
||||||
|
[field]: newValue,
|
||||||
|
index: newIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state === undefined) {
|
||||||
|
const id: Ref<IndexStageState> = generateId()
|
||||||
|
await storage.tx(ops.createTxCreateDoc(core.class.IndexStageState, plugin.space.DocIndexState, data, id))
|
||||||
|
state = {
|
||||||
|
...data,
|
||||||
|
_class: core.class.IndexStageState,
|
||||||
|
_id: id,
|
||||||
|
space: plugin.space.DocIndexState,
|
||||||
|
modifiedBy: core.account.System,
|
||||||
|
modifiedOn: Date.now()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await storage.tx(ops.createTxUpdateDoc(core.class.IndexStageState, plugin.space.DocIndexState, state._id, data))
|
||||||
|
state = { ...state, ...data, modifiedOn: Date.now() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [result, state]
|
||||||
|
}
|
||||||
|
@ -708,7 +708,8 @@ export async function createServerStorage (
|
|||||||
stages,
|
stages,
|
||||||
hierarchy,
|
hierarchy,
|
||||||
conf.workspace,
|
conf.workspace,
|
||||||
fulltextAdapter.metrics()
|
fulltextAdapter.metrics(),
|
||||||
|
modelDb
|
||||||
)
|
)
|
||||||
return new FullTextIndex(
|
return new FullTextIndex(
|
||||||
hierarchy,
|
hierarchy,
|
||||||
|
@ -53,7 +53,9 @@ class ElasticAdapter implements FullTextAdapter {
|
|||||||
const mappings = await this.client.indices.getMapping({
|
const mappings = await this.client.indices.getMapping({
|
||||||
index: toWorkspaceString(this.workspaceId)
|
index: toWorkspaceString(this.workspaceId)
|
||||||
})
|
})
|
||||||
console.log('Mapping', mappings.body)
|
if (field !== undefined) {
|
||||||
|
console.log('Mapping', mappings.body)
|
||||||
|
}
|
||||||
const wsMappings = mappings.body[toWorkspaceString(this.workspaceId)]
|
const wsMappings = mappings.body[toWorkspaceString(this.workspaceId)]
|
||||||
|
|
||||||
// Collect old values.
|
// Collect old values.
|
||||||
@ -80,7 +82,6 @@ class ElasticAdapter implements FullTextAdapter {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('Index created ok.')
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@ -195,7 +196,7 @@ class ElasticAdapter implements FullTextAdapter {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
script: {
|
script: {
|
||||||
source: `Math.abs(cosineSimilarity(params.queryVector, '${options.field}'))`,
|
source: `Math.abs(cosineSimilarity(params.queryVector, '${options.field}')) + 1`,
|
||||||
params: {
|
params: {
|
||||||
queryVector: embedding
|
queryVector: embedding
|
||||||
}
|
}
|
||||||
@ -215,10 +216,7 @@ class ElasticAdapter implements FullTextAdapter {
|
|||||||
filter: [
|
filter: [
|
||||||
{
|
{
|
||||||
bool: {
|
bool: {
|
||||||
should: [
|
must: [{ terms: this.getTerms(_classes, '_class') }]
|
||||||
{ terms: this.getTerms(_classes, '_class') },
|
|
||||||
{ terms: this.getTerms(_classes, 'attachedToClass') }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -239,7 +237,7 @@ class ElasticAdapter implements FullTextAdapter {
|
|||||||
const min = options?.minScore ?? 75
|
const min = options?.minScore ?? 75
|
||||||
const hits: any[] = sourceHits.filter((it: any) => it._score > min)
|
const hits: any[] = sourceHits.filter((it: any) => it._score > min)
|
||||||
|
|
||||||
return hits.map((hit) => ({ ...hit._source, _score: hit._score }))
|
return hits.map((hit) => ({ ...hit._source, _score: hit._score - (options.embeddingBoost ?? 100.0) }))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(JSON.stringify(err, null, 2))
|
console.error(JSON.stringify(err, null, 2))
|
||||||
return []
|
return []
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
DocIndexState,
|
DocIndexState,
|
||||||
DocumentQuery,
|
DocumentQuery,
|
||||||
DocumentUpdate,
|
DocumentUpdate,
|
||||||
|
IndexStageState,
|
||||||
MeasureContext,
|
MeasureContext,
|
||||||
Ref,
|
Ref,
|
||||||
Storage,
|
Storage,
|
||||||
@ -32,7 +33,8 @@ import {
|
|||||||
extractDocKey,
|
extractDocKey,
|
||||||
fieldStateId,
|
fieldStateId,
|
||||||
FullTextPipeline,
|
FullTextPipeline,
|
||||||
IndexedDoc
|
IndexedDoc,
|
||||||
|
loadIndexStageStage
|
||||||
} from '@hcengineering/server-core'
|
} from '@hcengineering/server-core'
|
||||||
|
|
||||||
import got from 'got'
|
import got from 'got'
|
||||||
@ -57,6 +59,10 @@ export class LibRetranslateStage implements TranslationStage {
|
|||||||
token: string = ''
|
token: string = ''
|
||||||
endpoint: string = ''
|
endpoint: string = ''
|
||||||
|
|
||||||
|
stageValue: boolean | string = true
|
||||||
|
|
||||||
|
indexState?: IndexStageState
|
||||||
|
|
||||||
constructor (readonly metrics: MeasureContext, readonly workspaceId: WorkspaceId) {}
|
constructor (readonly metrics: MeasureContext, readonly workspaceId: WorkspaceId) {}
|
||||||
|
|
||||||
async initialize (storage: Storage, pipeline: FullTextPipeline): Promise<void> {
|
async initialize (storage: Storage, pipeline: FullTextPipeline): Promise<void> {
|
||||||
@ -74,6 +80,11 @@ export class LibRetranslateStage implements TranslationStage {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
this.enabled = false
|
this.enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
;[this.stageValue, this.indexState] = await loadIndexStageStage(storage, this.indexState, this.stageId, 'config', {
|
||||||
|
enabled: this.enabled,
|
||||||
|
endpoint: this.endpoint
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async search (
|
async search (
|
||||||
@ -229,13 +240,13 @@ export class LibRetranslateStage implements TranslationStage {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await pipeline.update(doc._id, true, update, true)
|
await pipeline.update(doc._id, this.stageValue, update, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove (docs: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
async remove (docs: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
||||||
// will be handled by field processor
|
// will be handled by field processor
|
||||||
for (const doc of docs) {
|
for (const doc of docs) {
|
||||||
await pipeline.update(doc._id, true, {})
|
await pipeline.update(doc._id, this.stageValue, {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import { Configuration } from '@hcengineering/core'
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export const translateStateId = 'trn-v1a'
|
export const translateStateId = 'trn-v2'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
|
@ -202,30 +202,43 @@ class SessionManager {
|
|||||||
const sessions = Array.from(workspace.sessions)
|
const sessions = Array.from(workspace.sessions)
|
||||||
workspace.sessions = []
|
workspace.sessions = []
|
||||||
|
|
||||||
for (const s of sessions) {
|
const closeS = async (s: Session, webSocket: WebSocket): Promise<void> => {
|
||||||
// await for message to go to client.
|
// await for message to go to client.
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
// Override message handler, to wait for upgrading response from clients.
|
// Override message handler, to wait for upgrading response from clients.
|
||||||
s[1].on('close', () => {
|
webSocket.on('close', () => {
|
||||||
resolve(null)
|
resolve(null)
|
||||||
})
|
})
|
||||||
s[1].send(
|
webSocket.send(
|
||||||
serialize({
|
serialize({
|
||||||
result: {
|
result: {
|
||||||
_class: core.class.TxModelUpgrade
|
_class: core.class.TxModelUpgrade
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
setTimeout(resolve, 5000)
|
setTimeout(resolve, 1000)
|
||||||
})
|
})
|
||||||
s[1].close()
|
webSocket.close()
|
||||||
await this.setStatus(ctx, s[0], false)
|
await this.setStatus(ctx, s, false)
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await (await workspace.pipeline).close()
|
console.log(workspace.id, 'Clients disconnected. Closing Workspace...')
|
||||||
} catch (err: any) {
|
await Promise.all(sessions.map((s) => closeS(s[0], s[1])))
|
||||||
console.error(err)
|
|
||||||
|
const closePipeline = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await (await workspace.pipeline).close()
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
await Promise.race([
|
||||||
|
closePipeline,
|
||||||
|
new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 15000)
|
||||||
|
})
|
||||||
|
])
|
||||||
|
console.log(workspace.id, 'Workspace closed...')
|
||||||
}
|
}
|
||||||
|
|
||||||
async closeWorkspaces (ctx: MeasureContext): Promise<void> {
|
async closeWorkspaces (ctx: MeasureContext): Promise<void> {
|
||||||
|
Loading…
Reference in New Issue
Block a user