@things-factory/ai-assistant 10.1.88 → 10.1.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/assistant-chat.ts +45 -5
- package/client/components/assistant-dock.ts +30 -2
- package/client/components/entry-review.ts +246 -0
- package/client/components/workspace-view.ts +428 -0
- package/client/pages/assistant-workspace-page.ts +237 -0
- package/client/route.ts +9 -0
- package/client/utils/assistant-proposals.ts +11 -1
- package/client/utils/entry-proposal.ts +103 -0
- package/client/utils/workspace.ts +89 -0
- package/dist-client/components/assistant-chat.d.ts +3 -0
- package/dist-client/components/assistant-chat.js +46 -5
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.d.ts +1 -0
- package/dist-client/components/assistant-dock.js +25 -2
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/entry-review.d.ts +9 -0
- package/dist-client/components/entry-review.js +247 -0
- package/dist-client/components/entry-review.js.map +1 -0
- package/dist-client/components/workspace-view.d.ts +19 -0
- package/dist-client/components/workspace-view.js +416 -0
- package/dist-client/components/workspace-view.js.map +1 -0
- package/dist-client/pages/assistant-workspace-page.d.ts +27 -0
- package/dist-client/pages/assistant-workspace-page.js +263 -0
- package/dist-client/pages/assistant-workspace-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +10 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-proposals.d.ts +1 -0
- package/dist-client/utils/assistant-proposals.js +9 -1
- package/dist-client/utils/assistant-proposals.js.map +1 -1
- package/dist-client/utils/entry-proposal.d.ts +55 -0
- package/dist-client/utils/entry-proposal.js +52 -0
- package/dist-client/utils/entry-proposal.js.map +1 -0
- package/dist-client/utils/workspace.d.ts +40 -0
- package/dist-client/utils/workspace.js +68 -0
- package/dist-client/utils/workspace.js.map +1 -0
- package/dist-server/service/assistant-mode/registry.js +13 -0
- package/dist-server/service/assistant-mode/registry.js.map +1 -1
- package/dist-server/service/assistant-mode/resolve.d.ts +11 -0
- package/dist-server/service/assistant-mode/resolve.js +15 -3
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +7 -1
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.d.ts +46 -0
- package/dist-server/service/data-entry/entry-apply.js +102 -0
- package/dist-server/service/data-entry/entry-apply.js.map +1 -0
- package/dist-server/service/data-entry/entry-resolver.d.ts +4 -0
- package/dist-server/service/data-entry/entry-resolver.js +80 -0
- package/dist-server/service/data-entry/entry-resolver.js.map +1 -0
- package/dist-server/service/data-entry/entry-sources.d.ts +1 -0
- package/dist-server/service/data-entry/entry-sources.js +41 -0
- package/dist-server/service/data-entry/entry-sources.js.map +1 -0
- package/dist-server/service/data-entry/entry-target.d.ts +157 -0
- package/dist-server/service/data-entry/entry-target.js +343 -0
- package/dist-server/service/data-entry/entry-target.js.map +1 -0
- package/dist-server/service/data-entry/entry-tools.d.ts +43 -0
- package/dist-server/service/data-entry/entry-tools.js +185 -0
- package/dist-server/service/data-entry/entry-tools.js.map +1 -0
- package/dist-server/service/index.d.ts +6 -1
- package/dist-server/service/index.js +13 -2
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/workspace/workspace-blocks.d.ts +24 -0
- package/dist-server/service/workspace/workspace-blocks.js +118 -0
- package/dist-server/service/workspace/workspace-blocks.js.map +1 -0
- package/dist-server/service/workspace/workspace-tools.d.ts +24 -0
- package/dist-server/service/workspace/workspace-tools.js +65 -0
- package/dist-server/service/workspace/workspace-tools.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/assistant-mode/registry.ts +13 -0
- package/server/service/assistant-mode/resolve.test.ts +2 -1
- package/server/service/assistant-mode/resolve.ts +15 -3
- package/server/service/assistant-mode/types.ts +7 -1
- package/server/service/data-entry/data-entry.test.ts +311 -0
- package/server/service/data-entry/entry-apply.ts +134 -0
- package/server/service/data-entry/entry-resolver.ts +74 -0
- package/server/service/data-entry/entry-sources.ts +35 -0
- package/server/service/data-entry/entry-target.ts +442 -0
- package/server/service/data-entry/entry-tools.ts +174 -0
- package/server/service/index.ts +10 -1
- package/server/service/workspace/workspace-blocks.ts +127 -0
- package/server/service/workspace/workspace-tools.ts +66 -0
- package/server/service/workspace/workspace.test.ts +58 -0
- package/test/entry-proposal.test.ts +67 -0
- package/test/open-doors.test.ts +2 -1
- package/test/workspace.test.ts +54 -0
- package/things-factory.config.js +6 -1
- package/translations/en.json +56 -1
- package/translations/ja.json +56 -1
- package/translations/ko.json +56 -1
- package/translations/ms.json +56 -1
- package/translations/zh.json +56 -1
|
@@ -63,6 +63,9 @@ import { chatDefaults } from './chat-defaults.js'
|
|
|
63
63
|
import { isDifferentConversation, resolveConversation, type AssistantMode, type AssistantModeView, pickInitialMode } from './assistant-mode.js'
|
|
64
64
|
import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState, type ParsedProposalOutcome } from './proposal-outcome-line.js'
|
|
65
65
|
import { availableTriggers, type TriggerChip } from './chat-triggers.js'
|
|
66
|
+
import './entry-review.js'
|
|
67
|
+
import { entryReadyCount, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js'
|
|
68
|
+
import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js'
|
|
66
69
|
|
|
67
70
|
interface ChatLine {
|
|
68
71
|
role: 'user' | 'assistant' | 'system'
|
|
@@ -3945,12 +3948,19 @@ export class OxAssistantChat extends LitElement {
|
|
|
3945
3948
|
/* The card's state is the conversation's rows - read once for every card of this line. */
|
|
3946
3949
|
const outcomes = this.proposalOutcomes
|
|
3947
3950
|
const requested = this.proposalRequests
|
|
3951
|
+
/* A data entry card replaced by a newer one for the same record cannot be entered (design/ai/data-entry.md §5). */
|
|
3952
|
+
const latestEntry = items.some(isEntryProposal) ? latestEntryKeys(this.lines.map(one => one.proposals ?? [])) : undefined
|
|
3948
3953
|
return html`
|
|
3949
3954
|
<div class="proposals">
|
|
3950
3955
|
${items.map(p => {
|
|
3951
3956
|
const key = this.proposalKey(p)
|
|
3957
|
+
/* A workspace card opens the page; it runs nothing (design/ai/workspace.md §4). */
|
|
3958
|
+
if (isWorkspaceProposal(p)) return this.renderWorkspaceCard(p, key)
|
|
3952
3959
|
const card = proposalCardState(key, { outcomes, requested, sentHere: this.sentProposals.has(key) })
|
|
3953
|
-
const
|
|
3960
|
+
const entry = isEntryProposal(p)
|
|
3961
|
+
const replaced = entry && latestEntry?.get(p.target) !== key && card.state === 'ready'
|
|
3962
|
+
const ready = entry ? entryReadyCount(p) : 0
|
|
3963
|
+
const sent = card.state !== 'ready' || replaced || (entry && ready === 0)
|
|
3954
3964
|
/*
|
|
3955
3965
|
Follow-up chips are the host's words, never this component's. The fallback that used
|
|
3956
3966
|
to live here named a 3D modeller's operations in hardcoded Korean, and it reached the
|
|
@@ -3962,7 +3972,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
3962
3972
|
<div class="proposal ${sent ? 'sent' : ''}">
|
|
3963
3973
|
<md-icon>${p.icon || (p.source ? 'view_in_ar' : 'bolt')}</md-icon>
|
|
3964
3974
|
<span class="pbody">
|
|
3965
|
-
<span class="plabel">${p.label || p.command}</span>
|
|
3975
|
+
<span class="plabel">${entry && p.labelKey ? i18next.t(p.labelKey) : p.label || p.command}</span>
|
|
3966
3976
|
${p.reason ? html`<span class="preason">${p.reason}</span>` : nothing}
|
|
3967
3977
|
</span>
|
|
3968
3978
|
${/*
|
|
@@ -3984,9 +3994,15 @@ export class OxAssistantChat extends LitElement {
|
|
|
3984
3994
|
class="run"
|
|
3985
3995
|
?disabled=${sent}
|
|
3986
3996
|
@click=${() => this.executeProposal(p, key)}>
|
|
3987
|
-
${
|
|
3988
|
-
?
|
|
3989
|
-
:
|
|
3997
|
+
${replaced
|
|
3998
|
+
? i18next.t('ai-assistant.entry.replaced', { defaultValue: 'replaced by a newer card' })
|
|
3999
|
+
: card.state !== 'ready'
|
|
4000
|
+
? this.proposalStateText(key)
|
|
4001
|
+
: entry
|
|
4002
|
+
? ready
|
|
4003
|
+
? i18next.t('ai-assistant.entry.enter', { defaultValue: 'enter {count} rows', count: ready })
|
|
4004
|
+
: i18next.t('ai-assistant.entry.nothing-to-enter', { defaultValue: 'nothing to enter' })
|
|
4005
|
+
: i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
|
|
3990
4006
|
</button>
|
|
3991
4007
|
`}
|
|
3992
4008
|
${/*
|
|
@@ -4002,6 +4018,7 @@ export class OxAssistantChat extends LitElement {
|
|
|
4002
4018
|
</span>
|
|
4003
4019
|
` : nothing}
|
|
4004
4020
|
</div>
|
|
4021
|
+
${entry ? html`<ox-assistant-entry-review .proposal=${p}></ox-assistant-entry-review>` : nothing}
|
|
4005
4022
|
${rawChoices.length ? html`
|
|
4006
4023
|
<div class="proposal-choices" role="group" aria-label=${i18next.t('ai-assistant.text.proposal-choices', { defaultValue: 'follow-up choices' })}>
|
|
4007
4024
|
${rawChoices.map((c: any) => html`
|
|
@@ -4021,6 +4038,29 @@ export class OxAssistantChat extends LitElement {
|
|
|
4021
4038
|
`
|
|
4022
4039
|
}
|
|
4023
4040
|
|
|
4041
|
+
/** The AI 작업판 card - what it holds, which version, and the way to open it. The person opens it; the model does not. */
|
|
4042
|
+
private renderWorkspaceCard(p: any, key: string) {
|
|
4043
|
+
const { version, of } = versionOfCard(this.lines.map(one => one.proposals ?? []), p.workspace.name, key)
|
|
4044
|
+
const contents = Object.entries(p.contents ?? {})
|
|
4045
|
+
.map(([kind, count]) => String(i18next.t(`ai-assistant.workspace.contains-${kind}`, { defaultValue: `${kind} {count}`, count: Number(count) } as any)))
|
|
4046
|
+
.join(' · ')
|
|
4047
|
+
const open = () => {
|
|
4048
|
+
if (!this.sessionId) return
|
|
4049
|
+
/* The page is the application's; navigate is the shell's, reached by an event so this component stays standalone. */
|
|
4050
|
+
this.dispatchEvent(new CustomEvent('assistant-open-page', { detail: { href: workspaceAddress(this.sessionId, p.workspace.name, version) }, bubbles: true, composed: true }))
|
|
4051
|
+
}
|
|
4052
|
+
return html`
|
|
4053
|
+
<div class="proposal workspace">
|
|
4054
|
+
<md-icon>${p.icon || 'dashboard'}</md-icon>
|
|
4055
|
+
<span class="pbody">
|
|
4056
|
+
<span class="plabel">${p.workspace.title}</span>
|
|
4057
|
+
<span class="preason">${[contents, of > 1 ? i18next.t('ai-assistant.workspace.version', { defaultValue: 'version {version} of {of}', version, of }) : ''].filter(Boolean).join(' · ')}</span>
|
|
4058
|
+
</span>
|
|
4059
|
+
<button class="run" ?disabled=${!this.sessionId} @click=${open}>${i18next.t('ai-assistant.workspace.open', { defaultValue: 'open in workspace' })}</button>
|
|
4060
|
+
</div>
|
|
4061
|
+
`
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4024
4064
|
/**
|
|
4025
4065
|
* 눌린 카드가 말하는 것 — **호스트가 한 일**이고, 아직 모르면 모른다고 말한다.
|
|
4026
4066
|
*
|
|
@@ -12,7 +12,7 @@ import { client } from '@operato/graphql'
|
|
|
12
12
|
import { i18next, localize } from '@operato/i18n'
|
|
13
13
|
import { closeOverlay, notify } from '@operato/layout'
|
|
14
14
|
import { OxPrompt } from '@operato/popup'
|
|
15
|
-
import { AppController, store } from '@operato/shell'
|
|
15
|
+
import { AppController, navigate, store } from '@operato/shell'
|
|
16
16
|
import { ScrollbarStyles } from '@operato/styles'
|
|
17
17
|
|
|
18
18
|
import { followPickedConversation, type AssistantMode } from './assistant-mode.js'
|
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
type AnchorResult,
|
|
30
30
|
type AssistantSurface
|
|
31
31
|
} from '../utils/assistant-surface.js'
|
|
32
|
-
import { runProposal, type ProposalContext, type ProposalOutcome } from '../utils/assistant-proposals.js'
|
|
32
|
+
import { runProposal, setEntryProposalHandler, type ProposalContext, type ProposalOutcome } from '../utils/assistant-proposals.js'
|
|
33
|
+
import { entryProposalHandler, type EntryApplyAnswer } from '../utils/entry-proposal.js'
|
|
33
34
|
import { holdAssistantAsk, subscribeAssistantAsk, takeAssistantAsk, type AssistantAsk } from '../utils/assistant-ask.js'
|
|
34
35
|
import { surfaceTransition } from '../utils/assistant-navigation.js'
|
|
35
36
|
|
|
@@ -38,6 +39,25 @@ export const ASSISTANT_DOCK_VIEWPART = 'assistant-dock'
|
|
|
38
39
|
const t = (key: string, defaultValue: string, options: Record<string, unknown> = {}) =>
|
|
39
40
|
i18next.t(`ai-assistant.dock.${key}`, { defaultValue, ...options })
|
|
40
41
|
|
|
42
|
+
/* Pressing a data entry card asks the server to enter the rows it stored - the screen sends none (design/ai/data-entry.md §4). */
|
|
43
|
+
setEntryProposalHandler(
|
|
44
|
+
entryProposalHandler(
|
|
45
|
+
async (sessionId, proposalKey) => {
|
|
46
|
+
const result = await client.mutate({
|
|
47
|
+
mutation: gql`
|
|
48
|
+
mutation ($id: String!, $key: String!) {
|
|
49
|
+
applyEntryProposal(sessionId: $id, proposalKey: $key)
|
|
50
|
+
}
|
|
51
|
+
`,
|
|
52
|
+
variables: { id: sessionId, key: proposalKey }
|
|
53
|
+
})
|
|
54
|
+
if (result.errors?.length) throw new Error(result.errors[0].message)
|
|
55
|
+
return result.data?.applyEntryProposal as EntryApplyAnswer
|
|
56
|
+
},
|
|
57
|
+
(key, options) => String(i18next.t(key, options))
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
41
61
|
const MODES_QUERY = gql`
|
|
42
62
|
query ($subject: String!) {
|
|
43
63
|
assistantModes(subject: $subject) {
|
|
@@ -309,6 +329,12 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
309
329
|
|
|
310
330
|
private onStartNewSession = () => void this.newConversation()
|
|
311
331
|
|
|
332
|
+
/* A card asks for a page of this application - the AI 작업판. The dock stays open beside it. */
|
|
333
|
+
private onOpenPage = (event: CustomEvent) => {
|
|
334
|
+
const href = event.detail?.href
|
|
335
|
+
if (typeof href === 'string' && href) navigate(href)
|
|
336
|
+
}
|
|
337
|
+
|
|
312
338
|
/** Who reads the open conversation - absent until the list or the create answer says. */
|
|
313
339
|
private get audience(): string | undefined {
|
|
314
340
|
return this.sessions.find(session => session.id === this.sessionId)?.visibility || undefined
|
|
@@ -324,6 +350,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
324
350
|
connectedCallback() {
|
|
325
351
|
super.connectedCallback()
|
|
326
352
|
this.addEventListener('assistant-start-new-session', this.onStartNewSession)
|
|
353
|
+
this.addEventListener('assistant-open-page', this.onOpenPage as EventListener)
|
|
327
354
|
this.unsubscribers.push(
|
|
328
355
|
subscribeAssistantSurface(() => this.follow()),
|
|
329
356
|
store.subscribe(() => { if (this.openedIdentity !== this.identityKey()) this.follow() }),
|
|
@@ -339,6 +366,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
339
366
|
disconnectedCallback() {
|
|
340
367
|
this.revision++
|
|
341
368
|
this.removeEventListener('assistant-start-new-session', this.onStartNewSession)
|
|
369
|
+
this.removeEventListener('assistant-open-page', this.onOpenPage as EventListener)
|
|
342
370
|
this.unsubscribers.splice(0).forEach(unsubscribe => unsubscribe())
|
|
343
371
|
setOpenSession('')
|
|
344
372
|
this.controller.clear()
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The rows of a data entry proposal, as the person reviews them (operato-application/design/ai/data-entry.md §5).
|
|
3
|
+
*
|
|
4
|
+
* Read-only on purpose: a correction is said in the conversation ("row 2 is 12,000", "drop the last one")
|
|
5
|
+
* and the model proposes again. What is wrong in a cell is marked on the cell - blocking in the error
|
|
6
|
+
* colour, a note in the outline - and where each row came from is on the row.
|
|
7
|
+
*/
|
|
8
|
+
import '@material/web/icon/icon.js'
|
|
9
|
+
|
|
10
|
+
import { LitElement, css, html, nothing } from 'lit'
|
|
11
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
12
|
+
|
|
13
|
+
import { i18next } from '@operato/i18n'
|
|
14
|
+
|
|
15
|
+
import { entryCellIssues, entryIssueKey, type EntryIssueView, type EntryRowView, type EntryTotalView } from '../utils/entry-proposal.js'
|
|
16
|
+
|
|
17
|
+
const SHOWN = 50
|
|
18
|
+
|
|
19
|
+
const t = (key: string, defaultValue: string, options: Record<string, unknown> = {}) =>
|
|
20
|
+
i18next.t(`ai-assistant.entry.${key}`, { defaultValue, ...options })
|
|
21
|
+
|
|
22
|
+
function issueText(issue: EntryIssueView): string {
|
|
23
|
+
const params = { ...(issue.params ?? {}) } as any
|
|
24
|
+
if (Array.isArray(params.values)) params.values = params.values.join(', ')
|
|
25
|
+
return String(i18next.t(entryIssueKey(issue), { defaultValue: issue.code, ...params }))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function shown(value: unknown): string {
|
|
29
|
+
if (value === undefined || value === null) return ''
|
|
30
|
+
if (typeof value === 'number') return value.toLocaleString(i18next.language || undefined)
|
|
31
|
+
if (typeof value === 'boolean') return value ? '✓' : '—'
|
|
32
|
+
return String(value)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@customElement('ox-assistant-entry-review')
|
|
36
|
+
export class AssistantEntryReview extends LitElement {
|
|
37
|
+
static styles = css`
|
|
38
|
+
:host {
|
|
39
|
+
display: block;
|
|
40
|
+
margin: 4px 0 2px;
|
|
41
|
+
font-size: 12px;
|
|
42
|
+
color: var(--md-sys-color-on-surface);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.summary {
|
|
46
|
+
display: flex;
|
|
47
|
+
flex-wrap: wrap;
|
|
48
|
+
gap: 4px 10px;
|
|
49
|
+
margin-bottom: 6px;
|
|
50
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.summary .blocked {
|
|
54
|
+
color: var(--md-sys-color-error);
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.scroll {
|
|
59
|
+
max-height: 320px;
|
|
60
|
+
overflow: auto;
|
|
61
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
62
|
+
border-radius: 6px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
table {
|
|
66
|
+
width: 100%;
|
|
67
|
+
border-collapse: collapse;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
th,
|
|
71
|
+
td {
|
|
72
|
+
padding: 4px 6px;
|
|
73
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
74
|
+
text-align: start;
|
|
75
|
+
vertical-align: top;
|
|
76
|
+
white-space: nowrap;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
th {
|
|
80
|
+
position: sticky;
|
|
81
|
+
top: 0;
|
|
82
|
+
background: var(--md-sys-color-surface-container);
|
|
83
|
+
font-weight: 600;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
td.num {
|
|
87
|
+
text-align: end;
|
|
88
|
+
font-variant-numeric: tabular-nums;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
tr.blocked td {
|
|
92
|
+
background: color-mix(in srgb, var(--md-sys-color-error) 6%, transparent);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
td.bad {
|
|
96
|
+
color: var(--md-sys-color-error);
|
|
97
|
+
text-decoration: underline wavy var(--md-sys-color-error);
|
|
98
|
+
text-underline-offset: 3px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
td.note {
|
|
102
|
+
text-decoration: underline dotted var(--md-sys-color-outline);
|
|
103
|
+
text-underline-offset: 3px;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.missing {
|
|
107
|
+
color: var(--md-sys-color-error);
|
|
108
|
+
font-style: italic;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.source {
|
|
112
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
113
|
+
max-width: 180px;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.row-issue {
|
|
119
|
+
display: block;
|
|
120
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
121
|
+
white-space: normal;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.row-issue.blocking {
|
|
125
|
+
color: var(--md-sys-color-error);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.verified {
|
|
129
|
+
color: var(--md-sys-color-primary);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.verified md-icon,
|
|
133
|
+
.totals md-icon {
|
|
134
|
+
--md-icon-size: 14px;
|
|
135
|
+
vertical-align: -2px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.totals {
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-wrap: wrap;
|
|
141
|
+
gap: 2px 12px;
|
|
142
|
+
margin: 6px 0 0;
|
|
143
|
+
font-variant-numeric: tabular-nums;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.totals .differs {
|
|
147
|
+
color: var(--md-sys-color-error);
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.totals .matches {
|
|
152
|
+
color: var(--md-sys-color-primary);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.more,
|
|
156
|
+
.check-failed {
|
|
157
|
+
margin-top: 4px;
|
|
158
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
159
|
+
}
|
|
160
|
+
`
|
|
161
|
+
|
|
162
|
+
@property({ attribute: false }) proposal: any
|
|
163
|
+
|
|
164
|
+
/** The sum of each totalled field, beside the total the material prints - a missing row shows here without reading every row. */
|
|
165
|
+
private renderTotals(totals: EntryTotalView[], columns: { name: string; labelKey?: string }[]) {
|
|
166
|
+
if (!totals.length) return nothing
|
|
167
|
+
const name = (field: string) => {
|
|
168
|
+
const column = columns.find(one => one.name === field)
|
|
169
|
+
return column?.labelKey ? i18next.t(column.labelKey) : field
|
|
170
|
+
}
|
|
171
|
+
return html`<div class="totals" role="status">
|
|
172
|
+
${totals.map(total => html`
|
|
173
|
+
<span>${t('total', '{field} total {sum}', { field: name(total.field), sum: shown(total.all) })}</span>
|
|
174
|
+
${total.ready !== total.all ? html`<span>${t('total-ready', 'entered {sum}', { sum: shown(total.ready) })}</span>` : nothing}
|
|
175
|
+
${total.declared !== undefined
|
|
176
|
+
? html`<span>${t('total-declared', 'the material says {sum}', { sum: shown(total.declared) })}${total.declaredFound === false ? ` (${t('total-not-found', 'not found in the file')})` : ''}</span>
|
|
177
|
+
${total.difference
|
|
178
|
+
? html`<span class="differs">${t('total-differs', 'differs by {sum}', { sum: shown(total.difference) })}</span>`
|
|
179
|
+
: html`<span class="matches"><md-icon>task_alt</md-icon> ${t('total-matches', 'matches')}</span>`}`
|
|
180
|
+
: nothing}
|
|
181
|
+
`)}
|
|
182
|
+
</div>`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
render() {
|
|
186
|
+
const p = this.proposal ?? {}
|
|
187
|
+
const columns: { name: string; type: string; required?: boolean; labelKey?: string }[] = p.columns ?? []
|
|
188
|
+
const rows: EntryRowView[] = p.rows ?? []
|
|
189
|
+
const counts = p.counts ?? {}
|
|
190
|
+
return html`
|
|
191
|
+
<div class="summary" role="status">
|
|
192
|
+
<span>${t('rows', '{count} rows', { count: counts.rows ?? rows.length })}</span>
|
|
193
|
+
<span>${t('ready', '{count} ready', { count: counts.ready ?? 0 })}</span>
|
|
194
|
+
${counts.blocked ? html`<span class="blocked">${t('blocked', '{count} will not be entered', { count: counts.blocked })}</span>` : nothing}
|
|
195
|
+
${counts.cut ? html`<span class="blocked">${t('cut', '{count} left out over the limit', { count: counts.cut })}</span>` : nothing}
|
|
196
|
+
${counts.verified ? html`<span class="verified"><md-icon>task_alt</md-icon> ${t('verified', '{count} found in their file', { count: counts.verified })}</span>` : nothing}
|
|
197
|
+
${counts.unmatched ? html`<span class="blocked">${t('unmatched', '{count} with a value their file does not have', { count: counts.unmatched })}</span>` : nothing}
|
|
198
|
+
</div>
|
|
199
|
+
<div class="scroll">
|
|
200
|
+
<table>
|
|
201
|
+
<thead>
|
|
202
|
+
<tr>
|
|
203
|
+
<th>#</th>
|
|
204
|
+
${columns.map(column => html`<th>${column.labelKey ? i18next.t(column.labelKey) : column.name}${column.required ? ' *' : ''}</th>`)}
|
|
205
|
+
<th>${t('source', 'from')}</th>
|
|
206
|
+
</tr>
|
|
207
|
+
</thead>
|
|
208
|
+
<tbody>
|
|
209
|
+
${rows.slice(0, SHOWN).map(row => {
|
|
210
|
+
const blocked = (row.issues ?? []).some(issue => issue.blocking)
|
|
211
|
+
const whole = entryCellIssues(row)
|
|
212
|
+
return html`
|
|
213
|
+
<tr class=${blocked ? 'blocked' : ''}>
|
|
214
|
+
<td class="num">${row.index + 1}</td>
|
|
215
|
+
${columns.map(column => {
|
|
216
|
+
const issues = entryCellIssues(row, column.name)
|
|
217
|
+
const bad = issues.some(issue => issue.blocking)
|
|
218
|
+
const value = row.values?.[column.name]
|
|
219
|
+
const text = shown(value)
|
|
220
|
+
return html`<td
|
|
221
|
+
class="${column.type === 'number' || column.type === 'integer' ? 'num' : ''} ${bad ? 'bad' : issues.length ? 'note' : ''}"
|
|
222
|
+
title=${issues.map(issueText).join('\n')}
|
|
223
|
+
>
|
|
224
|
+
${text || (issues.some(issue => issue.code === 'required') ? html`<span class="missing">${t('missing', 'missing')}</span>` : '')}
|
|
225
|
+
</td>`
|
|
226
|
+
})}
|
|
227
|
+
<td class="source" title=${(row.sources ?? []).map(source => source.where || source.attachmentId).join(' · ')}>
|
|
228
|
+
${row.evidence ? html`<span class="verified" title=${t('evidence', 'found on line {line} of the file', { line: row.evidence.line })}><md-icon>task_alt</md-icon></span> ` : nothing}
|
|
229
|
+
${(row.sources ?? []).map(source => source.where || source.attachmentId).join(' · ')}
|
|
230
|
+
${whole
|
|
231
|
+
.filter(issue => issue.code !== 'no-source')
|
|
232
|
+
.map(issue => html`<span class="row-issue ${issue.blocking ? 'blocking' : ''}">${issueText(issue)}</span>`)}
|
|
233
|
+
${whole.some(issue => issue.code === 'no-source') ? html`<span class="row-issue">${issueText({ code: 'no-source', blocking: false })}</span>` : nothing}
|
|
234
|
+
</td>
|
|
235
|
+
</tr>
|
|
236
|
+
`
|
|
237
|
+
})}
|
|
238
|
+
</tbody>
|
|
239
|
+
</table>
|
|
240
|
+
</div>
|
|
241
|
+
${this.renderTotals(p.totals ?? [], columns)}
|
|
242
|
+
${rows.length > SHOWN ? html`<div class="more">${t('more-rows', '{count} more rows are not shown - they are entered too.', { count: rows.length - SHOWN })}</div>` : nothing}
|
|
243
|
+
${p.checkFailed ? html`<div class="check-failed">${t('check-failed', 'the application could not check these rows; they are checked again when entered.')}</div>` : nothing}
|
|
244
|
+
`
|
|
245
|
+
}
|
|
246
|
+
}
|