mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-09 01:10:17 +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
|
||||
|
||||
@Prop(TypeString(), attachment.string.Type)
|
||||
@Index(IndexKind.FullText)
|
||||
type!: string
|
||||
|
||||
@Prop(TypeTimestamp(), attachment.string.Date)
|
||||
|
@ -37,6 +37,7 @@ import {
|
||||
FullTextData,
|
||||
FullTextSearchContext,
|
||||
IndexKind,
|
||||
IndexStageState,
|
||||
Interface,
|
||||
Mixin,
|
||||
Obj,
|
||||
@ -251,7 +252,13 @@ export class TDocIndexState extends TDoc implements DocIndexState {
|
||||
removed!: boolean
|
||||
|
||||
// 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)
|
||||
|
@ -46,7 +46,8 @@ import {
|
||||
TTypeRelatedDocument,
|
||||
TTypeString,
|
||||
TTypeTimestamp,
|
||||
TVersion
|
||||
TVersion,
|
||||
TIndexStageState
|
||||
} from './core'
|
||||
import { TAccount, TSpace } from './security'
|
||||
import { TUserStatus } from './transient'
|
||||
@ -99,6 +100,7 @@ export function createModel (builder: Builder): void {
|
||||
TFulltextData,
|
||||
TTypeRelatedDocument,
|
||||
TDocIndexState,
|
||||
TIndexStageState,
|
||||
TFullTextSearchContext,
|
||||
TConfiguration,
|
||||
TConfigurationElement
|
||||
|
@ -23,6 +23,7 @@ import {
|
||||
Mixin,
|
||||
Model,
|
||||
Prop,
|
||||
ReadOnly,
|
||||
TypeBoolean,
|
||||
TypeDate,
|
||||
TypeMarkup,
|
||||
@ -34,14 +35,22 @@ import attachment from '@hcengineering/model-attachment'
|
||||
import calendar from '@hcengineering/model-calendar'
|
||||
import chunter from '@hcengineering/model-chunter'
|
||||
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 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 workbench, { Application, createNavigateAction } from '@hcengineering/model-workbench'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { Applicant, Candidate, Candidates, recruitId, Vacancy, VacancyList } from '@hcengineering/recruit'
|
||||
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||
import {
|
||||
Applicant,
|
||||
ApplicantMatch,
|
||||
Candidate,
|
||||
Candidates,
|
||||
recruitId,
|
||||
Vacancy,
|
||||
VacancyList
|
||||
} from '@hcengineering/recruit'
|
||||
import setting from '@hcengineering/setting'
|
||||
import { KeyBinding } from '@hcengineering/view'
|
||||
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)
|
||||
reviews?: number
|
||||
|
||||
@Prop(
|
||||
Collection(recruit.class.ApplicantMatch, getEmbeddedLabel('Vacancy match')),
|
||||
getEmbeddedLabel('Vacancy Matches')
|
||||
)
|
||||
vacancyMatch?: number
|
||||
}
|
||||
|
||||
@Mixin(recruit.mixin.VacancyList, contact.class.Organization)
|
||||
@ -136,8 +151,33 @@ export class TApplicant extends TTask implements Applicant {
|
||||
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 {
|
||||
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, {
|
||||
view: {
|
||||
@ -363,7 +403,7 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
]
|
||||
},
|
||||
recruit.viewlet.StatusTableApplicant
|
||||
recruit.viewlet.TableApplicant
|
||||
)
|
||||
builder.createDoc(
|
||||
view.class.Viewlet,
|
||||
@ -386,7 +426,19 @@ export function createModel (builder: Builder): void {
|
||||
],
|
||||
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> = {
|
||||
@ -445,6 +497,14 @@ export function createModel (builder: Builder): void {
|
||||
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, {
|
||||
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 {
|
||||
createNavigateAction(builder, key, label, recruit.app.Recruit as Ref<Application>, {
|
||||
application: recruitId,
|
||||
@ -822,6 +891,32 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
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'
|
||||
|
@ -88,7 +88,10 @@ export default mergeIds(recruitId, recruit, {
|
||||
NewCandidateHeader: '' as AnyComponent,
|
||||
ApplicantFilter: '' as AnyComponent,
|
||||
VacancyList: '' as AnyComponent,
|
||||
VacancyTemplateEditor: '' as AnyComponent
|
||||
VacancyTemplateEditor: '' as AnyComponent,
|
||||
ApplicationMatchPresenter: '' as AnyComponent,
|
||||
|
||||
MatchVacancy: '' as AnyComponent
|
||||
},
|
||||
template: {
|
||||
DefaultVacancy: '' as Ref<KanbanTemplate>,
|
||||
@ -103,8 +106,9 @@ export default mergeIds(recruitId, recruit, {
|
||||
viewlet: {
|
||||
TableCandidate: '' as Ref<Viewlet>,
|
||||
TableVacancy: '' as Ref<Viewlet>,
|
||||
StatusTableApplicant: '' as Ref<Viewlet>,
|
||||
ApplicantTable: '' as Ref<Viewlet>,
|
||||
TableApplicant: '' as Ref<Viewlet>,
|
||||
TableApplicantMatch: '' as Ref<Viewlet>,
|
||||
CalendarReview: '' as Ref<Viewlet>,
|
||||
TableReview: '' as Ref<Viewlet>
|
||||
}
|
||||
|
@ -350,7 +350,7 @@ export interface DocIndexState extends Doc {
|
||||
attachedToClass?: Ref<Class<Doc>>
|
||||
|
||||
// States for stages
|
||||
stages: Record<string, boolean>
|
||||
stages: Record<string, boolean | string>
|
||||
|
||||
removed: boolean
|
||||
|
||||
@ -362,6 +362,14 @@ export interface DocIndexState extends Doc {
|
||||
shortSummary?: Markup | null
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface IndexStageState extends Doc {
|
||||
stageId: string
|
||||
attributes: Record<string, any>
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*
|
||||
|
@ -41,7 +41,8 @@ import type {
|
||||
Type,
|
||||
UserStatus,
|
||||
Configuration,
|
||||
ConfigurationElement
|
||||
ConfigurationElement,
|
||||
IndexStageState
|
||||
} from './classes'
|
||||
import type {
|
||||
Tx,
|
||||
@ -102,6 +103,7 @@ export default plugin(coreId, {
|
||||
FulltextData: '' as Ref<Class<FullTextData>>,
|
||||
TypeRelatedDocument: '' as Ref<Class<Type<RelatedDocument>>>,
|
||||
DocIndexState: '' as Ref<Class<DocIndexState>>,
|
||||
IndexStageState: '' as Ref<Class<IndexStageState>>,
|
||||
|
||||
Configuration: '' as Ref<Class<Configuration>>
|
||||
},
|
||||
|
@ -26,7 +26,7 @@
|
||||
}
|
||||
let search = ''
|
||||
|
||||
$: summary = (indexDoc?.attributes as any).summary
|
||||
$: summary = indexDoc?.fullSummary ?? undefined
|
||||
|
||||
$: attributes =
|
||||
indexDoc !== undefined
|
||||
@ -51,10 +51,17 @@
|
||||
</script>
|
||||
|
||||
<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-doc text-base max-h-125">
|
||||
{#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:
|
||||
{#each summary.split('\n') as line}
|
||||
{@const hl = search.length > 0 && line.toLowerCase().includes(search.toLowerCase())}
|
||||
@ -74,6 +81,13 @@
|
||||
<div class="p-1 flex-row flex-wrap">
|
||||
{#each attr[1] as doc}
|
||||
<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}
|
||||
{@const hl = search.length > 0 && line.toLowerCase().includes(search.toLowerCase())}
|
||||
<span class:text-md={!hl} class:highlight={hl}>{line}</span>
|
||||
@ -100,7 +114,7 @@
|
||||
color: black;
|
||||
user-select: text;
|
||||
.highlight {
|
||||
color: red;
|
||||
color: blue;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -13,18 +13,18 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IconFolder, Label } from '@hcengineering/ui'
|
||||
import { IconFolder, IconSize, Label } from '@hcengineering/ui'
|
||||
|
||||
import type { Space } from '@hcengineering/core'
|
||||
import presentation from '..'
|
||||
|
||||
export let value: Space
|
||||
export let subtitle: string | undefined = undefined
|
||||
export let size: 'medium' | 'large'
|
||||
export let size: IconSize
|
||||
</script>
|
||||
|
||||
<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">
|
||||
{#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if}
|
||||
<div class="content-accent-color overflow-label">
|
||||
|
@ -45,13 +45,14 @@
|
||||
export let create: ObjectCreate | undefined = undefined
|
||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let size: ButtonSize = 'large'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let allowDeselect = false
|
||||
export let component: AnySvelteComponent | undefined = undefined
|
||||
export let componentProps: any | undefined = undefined
|
||||
export let autoSelect = true
|
||||
export let readonly = false
|
||||
|
||||
let selected: Space | undefined
|
||||
|
||||
@ -75,11 +76,15 @@
|
||||
$: updateSelected(value)
|
||||
|
||||
const showSpacesPopup = (ev: MouseEvent) => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
showPopup(
|
||||
SpacesPopup,
|
||||
{
|
||||
_class,
|
||||
label,
|
||||
size,
|
||||
allowDeselect,
|
||||
spaceOptions: { ...(spaceOptions ?? {}), sort: { ...(spaceOptions?.sort ?? {}), modifiedOn: -1 } },
|
||||
selected: selected?._id,
|
||||
@ -109,6 +114,7 @@
|
||||
<Button
|
||||
id="space.selector"
|
||||
{focus}
|
||||
disabled={readonly}
|
||||
{focusIndex}
|
||||
icon={IconFolder}
|
||||
{size}
|
||||
@ -118,7 +124,7 @@
|
||||
showTooltip={{ label, direction: labelDirection }}
|
||||
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}
|
||||
</span>
|
||||
</Button>
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
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 ObjectPopup from './ObjectPopup.svelte'
|
||||
import SpaceInfo from './SpaceInfo.svelte'
|
||||
@ -24,6 +24,7 @@
|
||||
export let spaceQuery: DocumentQuery<Space> | undefined
|
||||
export let spaceOptions: FindOptions<Space> | undefined = {}
|
||||
export let create: ObjectCreate | undefined = undefined
|
||||
export let size: ButtonSize = 'large'
|
||||
export let allowDeselect = false
|
||||
export let component: AnySvelteComponent | undefined = undefined
|
||||
export let componentProps: any | undefined = undefined
|
||||
@ -51,9 +52,9 @@
|
||||
>
|
||||
<svelte:fragment slot="item" let:item={space}>
|
||||
{#if component}
|
||||
<svelte:component this={component} {...componentProps} size={'large'} value={space} />
|
||||
<svelte:component this={component} {...componentProps} {size} value={space} />
|
||||
{:else}
|
||||
<SpaceInfo size={'large'} value={space} />
|
||||
<SpaceInfo {size} value={space} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ObjectPopup>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
import { IconSize } from '../../types'
|
||||
export let size: IconSize
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
|
@ -46,6 +46,7 @@
|
||||
|
||||
<div class="flex-row-center attachment-container">
|
||||
{#if openEmbedded(value.type)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="flex-center icon"
|
||||
on:click={() => {
|
||||
@ -71,6 +72,7 @@
|
||||
<div class="flex-center icon">
|
||||
{iconLabel(value.name)}
|
||||
{#if removable}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="remove-btn"
|
||||
on:click={(ev) => {
|
||||
@ -86,6 +88,7 @@
|
||||
{/if}
|
||||
<div class="flex-col info">
|
||||
{#if openEmbedded(value.type)}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="name"
|
||||
on:click={() => {
|
||||
|
@ -26,12 +26,14 @@
|
||||
</script>
|
||||
|
||||
{#if value && value > 0}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
use:tooltip={{
|
||||
label: attachment.string.Attachments,
|
||||
component: AttachmentPopup,
|
||||
props: { objectId: object._id, attachments: value }
|
||||
}}
|
||||
on:click|preventDefault|stopPropagation={() => {}}
|
||||
class="sm-tool-icon ml-1 mr-1"
|
||||
>
|
||||
<span class="icon"><IconAttachment {size} /></span>
|
||||
|
@ -26,12 +26,14 @@
|
||||
</script>
|
||||
|
||||
{#if value && value > 0}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
use:tooltip={{
|
||||
label: chunter.string.Comments,
|
||||
component: CommentPopup,
|
||||
props: { objectId: object._id }
|
||||
}}
|
||||
on:click|preventDefault|stopPropagation={() => {}}
|
||||
class="sm-tool-icon ml-1 mr-1"
|
||||
>
|
||||
<span class="icon"><IconThread {size} /></span>
|
||||
|
@ -99,7 +99,12 @@
|
||||
"HasNoActiveApplicant": "No Active",
|
||||
"NoneApplications": "None",
|
||||
"RelatedIssues": "Related issues",
|
||||
"VacancyList": "Vacancies"
|
||||
"VacancyList": "Vacancies",
|
||||
"MatchVacancy": "Match to vacancy",
|
||||
"VacancyMatching": "Match Talents to vacancy",
|
||||
"Score": "Score",
|
||||
"Match": "Match",
|
||||
"PerformMatch": "Match"
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Please select talent",
|
||||
|
@ -101,7 +101,12 @@
|
||||
"HasNoActiveApplicant": "Не активные",
|
||||
"NoneApplications": "Отсутствуют",
|
||||
"RelatedIssues": "Связанные задачи",
|
||||
"VacancyList": "Вакансии"
|
||||
"VacancyList": "Вакансии",
|
||||
"MatchVacancy": "Проверить на вакансию",
|
||||
"VacancyMatching": "Подбор кандидатов на вакансию",
|
||||
"Score": "Оценка",
|
||||
"Match": "Совпадение",
|
||||
"PerformMatch": "Сопоставить"
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Пожалуйста выберите таланта",
|
||||
|
@ -13,10 +13,23 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
|
||||
import chunter from '@hcengineering/chunter'
|
||||
import type { Contact, Employee, Person } from '@hcengineering/contact'
|
||||
import contact from '@hcengineering/contact'
|
||||
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 presentation, {
|
||||
Card,
|
||||
@ -32,6 +45,7 @@
|
||||
Button,
|
||||
ColorPopup,
|
||||
createFocusManager,
|
||||
deviceOptionsStore as deviceInfo,
|
||||
FocusHandler,
|
||||
getPlatformColor,
|
||||
Label,
|
||||
@ -44,13 +58,16 @@
|
||||
import CandidateCard from './CandidateCard.svelte'
|
||||
import VacancyCard from './VacancyCard.svelte'
|
||||
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
|
||||
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let candidate: Ref<Candidate>
|
||||
export let assignee: Ref<Employee>
|
||||
export let comment: Markup = ''
|
||||
|
||||
$: _comment = comment
|
||||
|
||||
export let preserveCandidate = false
|
||||
export let preserveVacancy = false
|
||||
|
||||
let status: Status = OK
|
||||
let createMore: boolean = false
|
||||
@ -133,12 +150,22 @@
|
||||
rank: calcRank(lastOne, undefined),
|
||||
startDate: 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) {
|
||||
// Prepare for next
|
||||
_candidate = '' as Ref<Candidate>
|
||||
_comment = ''
|
||||
doc = {
|
||||
state: selectedState?._id as Ref<State>,
|
||||
doneState: null,
|
||||
@ -256,6 +283,8 @@
|
||||
let verticalContent: boolean = false
|
||||
$: verticalContent = $deviceInfo.isMobile && $deviceInfo.isPortrait
|
||||
let btn: HTMLButtonElement
|
||||
|
||||
let descriptionBox: AttachmentStyledBox
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -306,6 +335,7 @@
|
||||
_class={recruit.class.Vacancy}
|
||||
spaceQuery={{ archived: false }}
|
||||
spaceOptions={orgOptions}
|
||||
readonly={preserveVacancy}
|
||||
label={recruit.string.Vacancy}
|
||||
create={{
|
||||
component: recruit.component.CreateVacancy,
|
||||
@ -324,6 +354,27 @@
|
||||
</SpaceSelect>
|
||||
</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">
|
||||
{#key doc}
|
||||
<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>
|
||||
{/if}
|
||||
{#if vacancy}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="name lines-limit-2"
|
||||
class:over-underline={!disabled}
|
||||
@ -95,22 +96,24 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if inline}
|
||||
<div class="flex-row-center">
|
||||
<VacancyIcon size={'small'} />
|
||||
<span class="ml-1">
|
||||
{vacancy.name}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
{vacancy.name}
|
||||
{/if}
|
||||
<div class="text-md">
|
||||
{#if inline}
|
||||
<div class="flex-row-center">
|
||||
<VacancyIcon size={'small'} />
|
||||
<span class="ml-1">
|
||||
{vacancy.name}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
{vacancy.name}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if company}
|
||||
<span class="label">{company.name}</span>
|
||||
{/if}
|
||||
{#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}
|
||||
|
||||
<div class="footer flex flex-reverse flex-grow">
|
||||
|
@ -62,6 +62,7 @@ import recruit from './plugin'
|
||||
import { objectIdProvider, objectLinkProvider, getApplicationTitle } from './utils'
|
||||
import VacancyList from './components/VacancyList.svelte'
|
||||
import VacancyTemplateEditor from './components/VacancyTemplateEditor.svelte'
|
||||
import MatchVacancy from './components/MatchVacancy.svelte'
|
||||
|
||||
async function createOpinion (object: Doc): Promise<void> {
|
||||
showPopup(CreateOpinion, { space: object.space, review: object._id })
|
||||
@ -300,7 +301,9 @@ export default async (): Promise<Resources> => ({
|
||||
ApplicantFilter,
|
||||
|
||||
VacancyList,
|
||||
VacancyTemplateEditor
|
||||
VacancyTemplateEditor,
|
||||
|
||||
MatchVacancy
|
||||
},
|
||||
completion: {
|
||||
ApplicationQuery: async (
|
||||
|
@ -111,7 +111,13 @@ export default mergeIds(recruitId, recruit, {
|
||||
HasActiveApplicant: '' as IntlString,
|
||||
HasNoActiveApplicant: '' 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: {
|
||||
CandidatesPublic: '' as Ref<Space>
|
||||
|
@ -89,6 +89,18 @@ export interface Applicant extends Task {
|
||||
comments?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ApplicantMatch extends AttachedDoc {
|
||||
attachedTo: Ref<Candidate>
|
||||
|
||||
complete: boolean
|
||||
vacancy: string
|
||||
summary: string
|
||||
response: string
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@ -129,6 +141,7 @@ const recruit = plugin(recruitId, {
|
||||
},
|
||||
class: {
|
||||
Applicant: '' as Ref<Class<Applicant>>,
|
||||
ApplicantMatch: '' as Ref<Class<ApplicantMatch>>,
|
||||
Candidates: '' as Ref<Class<Candidates>>,
|
||||
Vacancy: '' as Ref<Class<Vacancy>>,
|
||||
Review: '' as Ref<Class<Review>>,
|
||||
|
@ -61,6 +61,7 @@
|
||||
"FilteredViews": "Filtered views",
|
||||
"NewFilteredView": "New filtered view",
|
||||
"FilteredViewName": "Filtered view name",
|
||||
"Than": "Than"
|
||||
"Than": "Than",
|
||||
"ShowPreviewOnClick": "Please click to show document index preview..."
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,7 @@
|
||||
"FilteredViews": "Фильтрованные отображения",
|
||||
"NewFilteredView": "Новое фильтрованное отображение",
|
||||
"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 readonly = false
|
||||
|
||||
export let prefferedSorting: string = 'modifiedOn'
|
||||
|
||||
// If defined, will show a number of dummy items before real data will appear.
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
|
||||
@ -57,10 +59,16 @@
|
||||
|
||||
$: lookup = options?.lookup ?? buildConfigLookup(hierarchy, _class, config)
|
||||
|
||||
let sortKey = 'modifiedOn'
|
||||
let _sortKey = prefferedSorting
|
||||
$: if (!userSorting) {
|
||||
_sortKey = prefferedSorting
|
||||
}
|
||||
|
||||
let sortOrder = SortingOrder.Descending
|
||||
let loading = 0
|
||||
|
||||
let userSorting = false
|
||||
|
||||
let objects: Doc[] = []
|
||||
let objectsRecieved = false
|
||||
const refs: HTMLElement[] = []
|
||||
@ -71,7 +79,7 @@
|
||||
|
||||
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
|
||||
|
||||
async function update (
|
||||
@ -107,7 +115,7 @@
|
||||
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> => {
|
||||
selection = row
|
||||
@ -121,12 +129,13 @@
|
||||
})
|
||||
}
|
||||
|
||||
function changeSorting (key: string): void {
|
||||
function changeSorting (key: string | string[]): void {
|
||||
if (key === '') {
|
||||
return
|
||||
}
|
||||
if (key !== sortKey) {
|
||||
sortKey = key
|
||||
userSorting = true
|
||||
if (key !== _sortKey) {
|
||||
_sortKey = Array.isArray(key) ? key[0] : key
|
||||
sortOrder = SortingOrder.Ascending
|
||||
} else {
|
||||
sortOrder = sortOrder === SortingOrder.Ascending ? SortingOrder.Descending : SortingOrder.Ascending
|
||||
@ -211,14 +220,14 @@
|
||||
{#each model as attribute}
|
||||
<th
|
||||
class:sortable={attribute.sortingKey}
|
||||
class:sorted={attribute.sortingKey === sortKey}
|
||||
class:sorted={attribute.sortingKey === _sortKey}
|
||||
on:click={() => changeSorting(attribute.sortingKey)}
|
||||
>
|
||||
<div class="antiTable-cells">
|
||||
{#if attribute.label}
|
||||
<Label label={attribute.label} />
|
||||
{/if}
|
||||
{#if attribute.sortingKey === sortKey}
|
||||
{#if attribute.sortingKey === _sortKey}
|
||||
<div class="icon">
|
||||
{#if sortOrder === SortingOrder.Ascending}
|
||||
<IconUp size={'small'} />
|
||||
|
@ -14,12 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { Scroller, tableSP } from '@hcengineering/ui'
|
||||
import { BuildModelKey } from '@hcengineering/view'
|
||||
import { onMount } from 'svelte'
|
||||
import { ActionContext } from '..'
|
||||
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '../selection'
|
||||
import { LoadingProps } from '../utils'
|
||||
import SourcePresenter from './inference/SourcePresenter.svelte'
|
||||
import Table from './Table.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
@ -44,6 +46,32 @@
|
||||
onMount(() => {
|
||||
;(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>
|
||||
|
||||
<svelte:window />
|
||||
@ -57,7 +85,7 @@
|
||||
<Table
|
||||
bind:this={table}
|
||||
{_class}
|
||||
{config}
|
||||
config={_config}
|
||||
{options}
|
||||
{query}
|
||||
{showNotification}
|
||||
@ -66,6 +94,7 @@
|
||||
highlightRows={true}
|
||||
enableChecking
|
||||
checked={$selectionStore ?? []}
|
||||
{prefferedSorting}
|
||||
selection={listProvider.current($focusStore)}
|
||||
on:row-focus={(evt) => {
|
||||
listProvider.updateFocus(evt.detail)
|
||||
|
@ -15,7 +15,8 @@
|
||||
<script lang="ts">
|
||||
import { Doc, WithLookup } from '@hcengineering/core'
|
||||
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 search: string
|
||||
@ -23,7 +24,14 @@
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
use:tooltip={{ label: plugin.string.ShowPreviewOnClick }}
|
||||
on:click={() => {
|
||||
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 EnumEditor from './components/EnumEditor.svelte'
|
||||
import FilterBar from './components/filter/FilterBar.svelte'
|
||||
import FilterTypePopup from './components/filter/FilterTypePopup.svelte'
|
||||
import ObjectFilter from './components/filter/ObjectFilter.svelte'
|
||||
import TimestampFilter from './components/filter/TimestampFilter.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 HyperlinkPresenter from './components/HyperlinkPresenter.svelte'
|
||||
import IntlStringPresenter from './components/IntlStringPresenter.svelte'
|
||||
import GithubPresenter from './components/linkPresenters/GithubPresenter.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 NumberEditor from './components/NumberEditor.svelte'
|
||||
import NumberPresenter from './components/NumberPresenter.svelte'
|
||||
@ -47,31 +56,22 @@ import RolePresenter from './components/RolePresenter.svelte'
|
||||
import SpacePresenter from './components/SpacePresenter.svelte'
|
||||
import StringEditor from './components/StringEditor.svelte'
|
||||
import StringPresenter from './components/StringPresenter.svelte'
|
||||
import HyperlinkPresenter from './components/HyperlinkPresenter.svelte'
|
||||
import Table from './components/Table.svelte'
|
||||
import TableBrowser from './components/TableBrowser.svelte'
|
||||
import TimestampPresenter from './components/TimestampPresenter.svelte'
|
||||
import UpDownNavigator from './components/UpDownNavigator.svelte'
|
||||
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
||||
import ValueSelector from './components/ValueSelector.svelte'
|
||||
import HTMLEditor from './components/HTMLEditor.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 ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
||||
|
||||
import {
|
||||
afterResult,
|
||||
beforeResult,
|
||||
nestedDontMatchResult,
|
||||
nestedMatchResult,
|
||||
objectInResult,
|
||||
objectNinResult,
|
||||
valueInResult,
|
||||
valueNinResult,
|
||||
nestedMatchResult,
|
||||
nestedDontMatchResult
|
||||
valueNinResult
|
||||
} from './filter'
|
||||
|
||||
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 ActionHandler } from './components/ActionHandler.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 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 ObjectPresenter } from './components/ObjectPresenter.svelte'
|
||||
|
||||
export { default as List } from './components/list/List.svelte'
|
||||
export { default as TableBrowser } from './components/TableBrowser.svelte'
|
||||
export { default as ValueSelector } from './components/ValueSelector.svelte'
|
||||
export { default as MarkupPreviewPopup } from './components/MarkupPreviewPopup.svelte'
|
||||
export * from './context'
|
||||
export * from './filter'
|
||||
export * from './selection'
|
||||
export * from './viewOptions'
|
||||
export {
|
||||
buildModel,
|
||||
getActiveViewletId,
|
||||
getCollectionCounter,
|
||||
getFiltredKeys,
|
||||
getObjectPresenter,
|
||||
getObjectPreview,
|
||||
isCollectionAttr,
|
||||
LoadingProps,
|
||||
setActiveViewletId,
|
||||
getActiveViewletId,
|
||||
getFiltredKeys,
|
||||
isCollectionAttr
|
||||
setActiveViewletId
|
||||
} from './utils'
|
||||
export * from './viewOptions'
|
||||
export {
|
||||
HTMLPresenter,
|
||||
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,
|
||||
Ordering: '' 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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">
|
||||
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 {
|
||||
AnyComponent,
|
||||
@ -36,7 +36,6 @@
|
||||
setActiveViewletId,
|
||||
ViewletSettingButton
|
||||
} from '@hcengineering/view-resources'
|
||||
import SourcePresenter from './search/SourcePresenter.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
@ -139,20 +138,7 @@
|
||||
_class,
|
||||
space,
|
||||
options: viewlet.options,
|
||||
config: [
|
||||
...(search !== ''
|
||||
? [
|
||||
{
|
||||
key: '',
|
||||
presenter: SourcePresenter,
|
||||
label: getEmbeddedLabel('#'),
|
||||
sortingKey: '#score',
|
||||
props: { search }
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(preference?.config ?? viewlet.config)
|
||||
],
|
||||
config: preference?.config ?? viewlet.config,
|
||||
viewlet,
|
||||
viewOptions,
|
||||
createItemDialog: createComponent,
|
||||
|
@ -98,45 +98,60 @@ import { trackerId } from '@hcengineering/tracker'
|
||||
import { viewId } from '@hcengineering/view'
|
||||
import { workbenchId } from '@hcengineering/workbench'
|
||||
|
||||
addStringsLoader(loginId, async (lang: string) => await import(`@hcengineering/login-assets/lang/${lang}.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 loginEng from '@hcengineering/login-assets/lang/en.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
|
||||
*/
|
||||
|
@ -33,6 +33,7 @@
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server": "^0.6.4",
|
||||
"@hcengineering/chunter": "^0.6.2",
|
||||
"@hcengineering/recruit": "^0.6.4",
|
||||
"got": "^11.8.3",
|
||||
"fast-equals": "^2.0.3",
|
||||
"html-to-text": "^9.0.3"
|
||||
|
@ -20,6 +20,7 @@ import core, {
|
||||
DocumentQuery,
|
||||
DocumentUpdate,
|
||||
docUpdKey,
|
||||
IndexStageState,
|
||||
MeasureContext,
|
||||
Ref,
|
||||
Storage,
|
||||
@ -36,6 +37,7 @@ import {
|
||||
FullTextPipelineStage,
|
||||
IndexedDoc,
|
||||
isIndexingRequired,
|
||||
loadIndexStageStage,
|
||||
RateLimitter
|
||||
} from '@hcengineering/server-core'
|
||||
|
||||
@ -47,7 +49,7 @@ import openaiPlugin, { openAIRatelimitter } from './plugin'
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const openAIstage = 'emb-v3a'
|
||||
export const openAIstage = 'emb-v5'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -76,34 +78,37 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
||||
field = 'openai_embedding'
|
||||
field_enabled = '_use'
|
||||
|
||||
summary_field = 'summary'
|
||||
|
||||
enabled = false
|
||||
|
||||
clearExcept?: string[] = undefined
|
||||
updateFields: DocUpdateHandler[] = []
|
||||
|
||||
model = process.env.OPENAI_MODEL ?? 'text-embedding-ada-002'
|
||||
copyToState = true
|
||||
|
||||
model = 'text-embedding-ada-002'
|
||||
|
||||
tokenLimit = 8191
|
||||
|
||||
endpoint = process.env.OPENAI_HOST ?? 'https://api.openai.com/v1/embeddings'
|
||||
endpoint = 'https://api.openai.com/v1/embeddings'
|
||||
token = ''
|
||||
|
||||
rate = 5
|
||||
|
||||
stageValue: boolean | string = true
|
||||
|
||||
limitter = new RateLimitter(() => ({ rate: this.rate }))
|
||||
|
||||
indexState?: IndexStageState
|
||||
|
||||
async update (doc: DocIndexState, update: DocumentUpdate<DocIndexState>): Promise<void> {}
|
||||
|
||||
constructor (readonly adapter: FullTextAdapter, readonly metrics: MeasureContext, readonly workspaceId: WorkspaceId) {}
|
||||
|
||||
updateSummary (summary: FullSummaryStage): void {
|
||||
summary.fieldFilter.push((attr, value) => {
|
||||
if (
|
||||
attr.type._class === core.class.TypeMarkup &&
|
||||
(value.toLocaleLowerCase().startsWith('gpt:') || value.toLocaleLowerCase().startsWith('gpt Answer:'))
|
||||
) {
|
||||
const tMarkup = attr.type._class === core.class.TypeMarkup
|
||||
const lowerCase = value.toLocaleLowerCase()
|
||||
if (tMarkup && (lowerCase.includes('gpt:') || lowerCase.includes('gpt Answer:'))) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -150,6 +155,15 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
||||
console.error(err)
|
||||
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> {
|
||||
@ -232,17 +246,18 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
||||
}
|
||||
}
|
||||
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
|
||||
console.log('search embedding', embedding)
|
||||
const docs = await this.adapter.searchEmbedding(_classes, query, embedding, {
|
||||
size,
|
||||
from,
|
||||
minScore: 0,
|
||||
minScore: -100,
|
||||
embeddingBoost: 100,
|
||||
field: this.field,
|
||||
field_enable: this.field_enabled,
|
||||
fulltextBoost: 1
|
||||
fulltextBoost: 10
|
||||
})
|
||||
return {
|
||||
docs,
|
||||
@ -251,6 +266,9 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
||||
}
|
||||
|
||||
async collect (toIndex: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
||||
if (!this.enabled) {
|
||||
return
|
||||
}
|
||||
for (const doc of toIndex) {
|
||||
if (pipeline.cancelling) {
|
||||
return
|
||||
@ -274,13 +292,7 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
||||
|
||||
// No need to index this class, mark embeddings as empty ones.
|
||||
if (!needIndex) {
|
||||
await pipeline.update(doc._id, true, {})
|
||||
return
|
||||
}
|
||||
|
||||
if (this.token === '') {
|
||||
// No token, just do nothing.
|
||||
await pipeline.update(doc._id, true, {})
|
||||
await pipeline.update(doc._id, this.stageValue, {})
|
||||
return
|
||||
}
|
||||
|
||||
@ -288,10 +300,11 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
||||
if (this.unauthorized) {
|
||||
return
|
||||
}
|
||||
const embeddingText = (doc.attributes[this.summary_field] as string) ?? ''
|
||||
const embeddingText = doc.fullSummary ?? ''
|
||||
|
||||
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)
|
||||
|
||||
@ -322,7 +335,11 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
||||
[this.field]: embedding,
|
||||
[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
|
||||
}
|
||||
} catch (err: any) {
|
||||
@ -337,18 +354,15 @@ export class OpenAIEmbeddingsStage implements FullTextPipelineStage {
|
||||
}
|
||||
// Print error only first time, and update it in doc index
|
||||
console.error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// We need to collect all fields and prepare embedding document.
|
||||
|
||||
await pipeline.update(doc._id, true, update)
|
||||
await pipeline.update(doc._id, this.stageValue, update)
|
||||
}
|
||||
|
||||
async remove (docs: DocIndexState[], pipeline: FullTextPipeline): Promise<void> {
|
||||
// will be handled by field processor
|
||||
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,
|
||||
TxCreateDoc,
|
||||
TxCUD,
|
||||
TxProcessor,
|
||||
TxUpdateDoc
|
||||
TxProcessor
|
||||
} from '@hcengineering/core'
|
||||
import type { TriggerControl } from '@hcengineering/server-core'
|
||||
import got from 'got'
|
||||
import { convert } from 'html-to-text'
|
||||
import { encode } from './encoder/encoder'
|
||||
import openai, { openAIRatelimitter } from './plugin'
|
||||
import { chunks, encode } from './encoder/encoder'
|
||||
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
|
||||
*/
|
||||
@ -43,156 +107,200 @@ export async function OnGPTRequest (tx: Tx, tc: TriggerControl): Promise<Tx[]> {
|
||||
const cud: TxCUD<Doc> = actualTx as TxCUD<Doc>
|
||||
//
|
||||
if (tc.hierarchy.isDerived(cud.objectClass, chunter.class.Comment)) {
|
||||
let msg = ''
|
||||
//
|
||||
if (actualTx._class === core.class.TxCreateDoc) {
|
||||
msg = (cud as TxCreateDoc<Comment>).attributes.message
|
||||
} 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 await handleComment(tx, tc)
|
||||
}
|
||||
if (tc.hierarchy.isDerived(cud.objectClass, recruit.class.ApplicantMatch)) {
|
||||
return await handleApplicantMatch(tx, tc)
|
||||
}
|
||||
}
|
||||
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
|
||||
*/
|
||||
@ -201,3 +309,30 @@ export const openAIPluginImpl = async () => ({
|
||||
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
|
||||
|
||||
stageValue: boolean | string = true
|
||||
|
||||
constructor (
|
||||
readonly storageAdapter: MinioService | undefined,
|
||||
readonly workspace: WorkspaceId,
|
||||
|
@ -50,6 +50,8 @@ export class IndexedFieldStage implements FullTextPipelineStage {
|
||||
|
||||
enabled = true
|
||||
|
||||
stageValue: boolean | string = true
|
||||
|
||||
constructor (private readonly dbStorage: ServerStorage, readonly metrics: MeasureContext) {}
|
||||
|
||||
async initialize (storage: Storage, pipeline: FullTextPipeline): Promise<void> {
|
||||
|
@ -54,6 +54,8 @@ export class FullTextPushStage implements FullTextPipelineStage {
|
||||
|
||||
field_enabled = '_use'
|
||||
|
||||
stageValue: boolean | string = true
|
||||
|
||||
constructor (
|
||||
readonly fulltextAdapter: FullTextAdapter,
|
||||
readonly workspace: WorkspaceId,
|
||||
|
@ -23,6 +23,7 @@ import core, {
|
||||
DOMAIN_DOC_INDEX_STATE,
|
||||
Hierarchy,
|
||||
MeasureContext,
|
||||
ModelDb,
|
||||
Ref,
|
||||
ServerStorage,
|
||||
setObjectValue,
|
||||
@ -77,7 +78,8 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
private readonly stages: FullTextPipelineStage[],
|
||||
readonly hierarchy: Hierarchy,
|
||||
readonly workspace: WorkspaceId,
|
||||
readonly metrics: MeasureContext
|
||||
readonly metrics: MeasureContext,
|
||||
readonly model: ModelDb
|
||||
) {
|
||||
this.readyStages = stages.map((it) => it.stageId)
|
||||
this.readyStages.sort()
|
||||
@ -101,6 +103,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
): Promise<{ docs: IndexedDoc[], pass: boolean }> {
|
||||
const result: IndexedDoc[] = []
|
||||
for (const st of this.stages) {
|
||||
await st.initialize(this.storage, this)
|
||||
const docs = await st.search(_classes, search, size, from)
|
||||
result.push(...docs.docs)
|
||||
if (!docs.pass) {
|
||||
@ -154,7 +157,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
// Update are commulative
|
||||
async update (
|
||||
docId: Ref<DocIndexState>,
|
||||
mark: boolean,
|
||||
mark: boolean | string,
|
||||
update: DocumentUpdate<DocIndexState>,
|
||||
flush?: boolean
|
||||
): Promise<void> {
|
||||
@ -162,7 +165,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
if (udoc !== undefined) {
|
||||
await this.stageUpdate(udoc, update)
|
||||
|
||||
udoc = this.updateDoc(udoc, update, mark)
|
||||
udoc = this.updateDoc(udoc, update, mark !== false)
|
||||
this.toIndex.set(docId, udoc)
|
||||
}
|
||||
|
||||
@ -170,7 +173,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
udoc = this.toIndexParents.get(docId)
|
||||
if (udoc !== undefined) {
|
||||
await this.stageUpdate(udoc, update)
|
||||
udoc = this.updateDoc(udoc, update, mark)
|
||||
udoc = this.updateDoc(udoc, update, mark !== false)
|
||||
this.toIndexParents.set(docId, udoc)
|
||||
}
|
||||
}
|
||||
@ -294,7 +297,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
const result = await this.storage.findAll(
|
||||
core.class.DocIndexState,
|
||||
{
|
||||
[`stages.${st.stageId}`]: { $nin: [true] },
|
||||
[`stages.${st.stageId}`]: { $nin: [st.stageValue] },
|
||||
_id: { $nin: toSkip },
|
||||
removed: false
|
||||
},
|
||||
@ -346,7 +349,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
|
||||
// Check items with not updated state.
|
||||
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)
|
||||
} else {
|
||||
this.skipped.delete(d._id)
|
||||
@ -420,7 +423,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
}
|
||||
for (const st of statistics) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -20,23 +20,24 @@ import core, {
|
||||
DocIndexState,
|
||||
DocumentQuery,
|
||||
DocumentUpdate,
|
||||
docUpdKey,
|
||||
extractDocKey,
|
||||
FullTextSearchContext,
|
||||
Hierarchy,
|
||||
IndexStageState,
|
||||
isFullTextAttribute,
|
||||
Ref,
|
||||
Storage
|
||||
} from '@hcengineering/core'
|
||||
import { translate } from '@hcengineering/platform'
|
||||
import { convert } from 'html-to-text'
|
||||
import { IndexedDoc } from '../types'
|
||||
import { contentStageId, DocUpdateHandler, fieldStateId, FullTextPipeline, FullTextPipelineStage } from './types'
|
||||
import { convert } from 'html-to-text'
|
||||
import { loadIndexStageStage } from './utils'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const summaryStageId = 'sum-v2'
|
||||
export const summaryStageId = 'sum-v3a'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -54,11 +55,22 @@ export class FullSummaryStage implements FullTextPipelineStage {
|
||||
// If specified, index only fields with content speciffied.
|
||||
matchExtra: string[] = [] // 'content', 'base64'] // '#en'
|
||||
|
||||
summaryField = 'summary'
|
||||
|
||||
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 (
|
||||
_classes: Ref<Class<Doc>>[],
|
||||
@ -79,7 +91,7 @@ export class FullSummaryStage implements FullTextPipelineStage {
|
||||
|
||||
// No need to index this class, mark embeddings as empty ones.
|
||||
if (!needIndex) {
|
||||
await pipeline.update(doc._id, true, {})
|
||||
await pipeline.update(doc._id, this.stageValue, {})
|
||||
continue
|
||||
}
|
||||
|
||||
@ -89,16 +101,16 @@ export class FullSummaryStage implements FullTextPipelineStage {
|
||||
matchExtra: this.matchExtra,
|
||||
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> {
|
||||
// will be handled by field processor
|
||||
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)) {
|
||||
continue
|
||||
}
|
||||
if (keyAttr.type._class === core.class.TypeAttachment && extra.length === 0) {
|
||||
// Skipt attachment id values.
|
||||
continue
|
||||
}
|
||||
|
||||
const repl = extra.join('#')
|
||||
|
||||
|
@ -13,7 +13,17 @@
|
||||
// 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'
|
||||
|
||||
/**
|
||||
@ -21,9 +31,10 @@ import type { IndexedDoc } from '../types'
|
||||
*/
|
||||
export interface FullTextPipeline {
|
||||
hierarchy: Hierarchy
|
||||
model: ModelDb
|
||||
update: (
|
||||
docId: Ref<DocIndexState>,
|
||||
mark: boolean,
|
||||
mark: boolean | string,
|
||||
update: DocumentUpdate<DocIndexState>,
|
||||
flush?: boolean
|
||||
) => Promise<void>
|
||||
@ -61,6 +72,8 @@ export interface FullTextPipelineStage {
|
||||
|
||||
enabled: boolean
|
||||
|
||||
stageValue: boolean | string
|
||||
|
||||
initialize: (storage: Storage, pipeline: FullTextPipeline) => Promise<void>
|
||||
|
||||
// Collect all changes related to bulk of document states
|
||||
|
@ -28,12 +28,17 @@ import core, {
|
||||
DOMAIN_MODEL,
|
||||
DOMAIN_TRANSIENT,
|
||||
DOMAIN_TX,
|
||||
generateId,
|
||||
Hierarchy,
|
||||
IndexStageState,
|
||||
isFullTextAttribute,
|
||||
Obj,
|
||||
Ref,
|
||||
Space
|
||||
Space,
|
||||
Storage,
|
||||
TxFactory
|
||||
} from '@hcengineering/core'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import plugin from '../plugin'
|
||||
/**
|
||||
* @public
|
||||
@ -155,3 +160,52 @@ export function createStateDoc (
|
||||
...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,
|
||||
hierarchy,
|
||||
conf.workspace,
|
||||
fulltextAdapter.metrics()
|
||||
fulltextAdapter.metrics(),
|
||||
modelDb
|
||||
)
|
||||
return new FullTextIndex(
|
||||
hierarchy,
|
||||
|
@ -53,7 +53,9 @@ class ElasticAdapter implements FullTextAdapter {
|
||||
const mappings = await this.client.indices.getMapping({
|
||||
index: toWorkspaceString(this.workspaceId)
|
||||
})
|
||||
console.log('Mapping', mappings.body)
|
||||
if (field !== undefined) {
|
||||
console.log('Mapping', mappings.body)
|
||||
}
|
||||
const wsMappings = mappings.body[toWorkspaceString(this.workspaceId)]
|
||||
|
||||
// Collect old values.
|
||||
@ -80,7 +82,6 @@ class ElasticAdapter implements FullTextAdapter {
|
||||
})
|
||||
}
|
||||
}
|
||||
console.log('Index created ok.')
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
@ -195,7 +196,7 @@ class ElasticAdapter implements FullTextAdapter {
|
||||
}
|
||||
},
|
||||
script: {
|
||||
source: `Math.abs(cosineSimilarity(params.queryVector, '${options.field}'))`,
|
||||
source: `Math.abs(cosineSimilarity(params.queryVector, '${options.field}')) + 1`,
|
||||
params: {
|
||||
queryVector: embedding
|
||||
}
|
||||
@ -215,10 +216,7 @@ class ElasticAdapter implements FullTextAdapter {
|
||||
filter: [
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ terms: this.getTerms(_classes, '_class') },
|
||||
{ terms: this.getTerms(_classes, 'attachedToClass') }
|
||||
]
|
||||
must: [{ terms: this.getTerms(_classes, '_class') }]
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -239,7 +237,7 @@ class ElasticAdapter implements FullTextAdapter {
|
||||
const min = options?.minScore ?? 75
|
||||
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) {
|
||||
console.error(JSON.stringify(err, null, 2))
|
||||
return []
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
DocIndexState,
|
||||
DocumentQuery,
|
||||
DocumentUpdate,
|
||||
IndexStageState,
|
||||
MeasureContext,
|
||||
Ref,
|
||||
Storage,
|
||||
@ -32,7 +33,8 @@ import {
|
||||
extractDocKey,
|
||||
fieldStateId,
|
||||
FullTextPipeline,
|
||||
IndexedDoc
|
||||
IndexedDoc,
|
||||
loadIndexStageStage
|
||||
} from '@hcengineering/server-core'
|
||||
|
||||
import got from 'got'
|
||||
@ -57,6 +59,10 @@ export class LibRetranslateStage implements TranslationStage {
|
||||
token: string = ''
|
||||
endpoint: string = ''
|
||||
|
||||
stageValue: boolean | string = true
|
||||
|
||||
indexState?: IndexStageState
|
||||
|
||||
constructor (readonly metrics: MeasureContext, readonly workspaceId: WorkspaceId) {}
|
||||
|
||||
async initialize (storage: Storage, pipeline: FullTextPipeline): Promise<void> {
|
||||
@ -74,6 +80,11 @@ export class LibRetranslateStage implements TranslationStage {
|
||||
console.error(err)
|
||||
this.enabled = false
|
||||
}
|
||||
|
||||
;[this.stageValue, this.indexState] = await loadIndexStageStage(storage, this.indexState, this.stageId, 'config', {
|
||||
enabled: this.enabled,
|
||||
endpoint: this.endpoint
|
||||
})
|
||||
}
|
||||
|
||||
async search (
|
||||
@ -229,13 +240,13 @@ export class LibRetranslateStage implements TranslationStage {
|
||||
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> {
|
||||
// will be handled by field processor
|
||||
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
|
||||
*/
|
||||
export const translateStateId = 'trn-v1a'
|
||||
export const translateStateId = 'trn-v2'
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@ -202,30 +202,43 @@ class SessionManager {
|
||||
const sessions = Array.from(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 new Promise((resolve) => {
|
||||
// Override message handler, to wait for upgrading response from clients.
|
||||
s[1].on('close', () => {
|
||||
webSocket.on('close', () => {
|
||||
resolve(null)
|
||||
})
|
||||
s[1].send(
|
||||
webSocket.send(
|
||||
serialize({
|
||||
result: {
|
||||
_class: core.class.TxModelUpgrade
|
||||
}
|
||||
})
|
||||
)
|
||||
setTimeout(resolve, 5000)
|
||||
setTimeout(resolve, 1000)
|
||||
})
|
||||
s[1].close()
|
||||
await this.setStatus(ctx, s[0], false)
|
||||
webSocket.close()
|
||||
await this.setStatus(ctx, s, false)
|
||||
}
|
||||
try {
|
||||
await (await workspace.pipeline).close()
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
|
||||
console.log(workspace.id, 'Clients disconnected. Closing Workspace...')
|
||||
await Promise.all(sessions.map((s) => closeS(s[0], s[1])))
|
||||
|
||||
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> {
|
||||
|
Loading…
Reference in New Issue
Block a user