@things-factory/ai-assistant 10.1.223 → 10.1.227
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 +50 -2
- package/client/components/assistant-dock.ts +1 -1
- package/client/components/assistant-mode.ts +1 -1
- package/client/components/entry-review.ts +2 -2
- package/client/components/workspace-view.ts +1 -1
- package/client/index.ts +1 -1
- package/client/pages/assistant-workspace-page.ts +1 -1
- package/client/route.ts +1 -1
- package/client/utils/attachment-policy.ts +15 -8
- package/client/utils/document-text.ts +332 -0
- package/client/utils/entry-hash.ts +1 -1
- package/client/utils/entry-proposal.ts +1 -1
- package/client/utils/entry-workspace.ts +1 -1
- package/client/utils/pdf-pages.ts +38 -0
- package/client/utils/workspace.ts +1 -1
- package/dist-client/components/assistant-chat.d.ts +3 -1
- package/dist-client/components/assistant-chat.js +48 -2
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.js +1 -1
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/assistant-mode.d.ts +1 -1
- package/dist-client/components/assistant-mode.js.map +1 -1
- package/dist-client/components/entry-review.d.ts +1 -1
- package/dist-client/components/entry-review.js +2 -2
- package/dist-client/components/entry-review.js.map +1 -1
- package/dist-client/components/workspace-view.js +1 -1
- package/dist-client/components/workspace-view.js.map +1 -1
- package/dist-client/index.js +1 -1
- package/dist-client/index.js.map +1 -1
- package/dist-client/pages/assistant-workspace-page.js +1 -1
- package/dist-client/pages/assistant-workspace-page.js.map +1 -1
- package/dist-client/route.js +1 -1
- package/dist-client/route.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/attachment-policy.d.ts +2 -2
- package/dist-client/utils/attachment-policy.js +9 -5
- package/dist-client/utils/attachment-policy.js.map +1 -1
- package/dist-client/utils/document-text.d.ts +118 -0
- package/dist-client/utils/document-text.js +320 -0
- package/dist-client/utils/document-text.js.map +1 -0
- package/dist-client/utils/entry-hash.js +1 -1
- package/dist-client/utils/entry-hash.js.map +1 -1
- package/dist-client/utils/entry-proposal.js +1 -1
- package/dist-client/utils/entry-proposal.js.map +1 -1
- package/dist-client/utils/entry-workspace.js +1 -1
- package/dist-client/utils/entry-workspace.js.map +1 -1
- package/dist-client/utils/pdf-pages.d.ts +6 -0
- package/dist-client/utils/pdf-pages.js +34 -0
- package/dist-client/utils/pdf-pages.js.map +1 -0
- package/dist-client/utils/workspace.js +1 -1
- package/dist-client/utils/workspace.js.map +1 -1
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js +1 -1
- package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.d.ts +3 -3
- package/dist-server/service/assistant-mode/mode-categories.js +4 -4
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/service/chat-session/attachment-policy.d.ts +1 -1
- package/dist-server/service/chat-session/attachment-policy.js +2 -2
- package/dist-server/service/chat-session/attachment-policy.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.d.ts +1 -1
- package/dist-server/service/data-entry/entry-apply.js +1 -1
- package/dist-server/service/data-entry/entry-apply.js.map +1 -1
- package/dist-server/service/data-entry/entry-hash.js +1 -1
- package/dist-server/service/data-entry/entry-hash.js.map +1 -1
- package/dist-server/service/data-read/read-target.d.ts +1 -1
- package/dist-server/service/data-read/read-target.js +2 -2
- package/dist-server/service/data-read/read-target.js.map +1 -1
- package/dist-server/service/data-read/read-tools.js +5 -5
- package/dist-server/service/data-read/read-tools.js.map +1 -1
- package/dist-server/service/index.js +1 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/people/index.js +1 -1
- package/dist-server/service/people/index.js.map +1 -1
- package/dist-server/service/workspace/workspace-blocks.js +1 -1
- package/dist-server/service/workspace/workspace-blocks.js.map +1 -1
- package/dist-server/service/workspace/workspace-tools.js +1 -1
- package/dist-server/service/workspace/workspace-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -9
- package/server/service/assistant-mode/assistant-mode-resolver.ts +1 -1
- package/server/service/assistant-mode/mode-categories.ts +4 -4
- package/server/service/assistant-mode/mode-front.test.ts +1 -1
- package/server/service/assistant-mode/resolve.test.ts +1 -1
- package/server/service/chat-session/attachment-policy.ts +7 -5
- package/server/service/data-entry/entry-apply.ts +2 -2
- package/server/service/data-entry/entry-hash.ts +1 -1
- package/server/service/data-read/read-target.ts +2 -2
- package/server/service/data-read/read-tools.ts +5 -5
- package/server/service/data-read/read-words.test.ts +1 -1
- package/server/service/index.ts +1 -1
- package/server/service/people/index.ts +1 -1
- package/server/service/unattended-run/run-unattended.test.ts +1 -1
- package/server/service/workspace/workspace-blocks.ts +1 -1
- package/server/service/workspace/workspace-tools.ts +1 -1
- package/server/service/workspace/workspace.test.ts +1 -1
- package/test/attachment-policy-agree.test.ts +4 -1
- package/test/document-text.test.ts +175 -0
- package/test/entry-workspace.test.ts +1 -1
- package/test/workspace.test.ts +1 -1
- package/translations/en.json +6 -0
- package/translations/es.json +6 -0
- package/translations/ja.json +6 -0
- package/translations/ko.json +16 -10
- package/translations/ms.json +6 -0
- package/translations/zh.json +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/ai-assistant",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.227",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"things-factory": true,
|
|
6
6
|
"author": "heartyoh",
|
|
@@ -35,25 +35,27 @@
|
|
|
35
35
|
"@operato/shell": "^10.29.39",
|
|
36
36
|
"@operato/styles": "^10.0.0",
|
|
37
37
|
"@operato/utils": "^10.29.39",
|
|
38
|
-
"@things-factory/ai-client-base": "^10.1.
|
|
39
|
-
"@things-factory/apptool-base": "^10.1.
|
|
40
|
-
"@things-factory/attachment-base": "^10.1.
|
|
41
|
-
"@things-factory/auth-base": "^10.1.
|
|
38
|
+
"@things-factory/ai-client-base": "^10.1.227",
|
|
39
|
+
"@things-factory/apptool-base": "^10.1.227",
|
|
40
|
+
"@things-factory/attachment-base": "^10.1.227",
|
|
41
|
+
"@things-factory/auth-base": "^10.1.227",
|
|
42
42
|
"@things-factory/env": "^10.1.149",
|
|
43
|
-
"@things-factory/setting-base": "^10.1.
|
|
44
|
-
"@things-factory/shell": "^10.1.
|
|
43
|
+
"@things-factory/setting-base": "^10.1.227",
|
|
44
|
+
"@things-factory/shell": "^10.1.227",
|
|
45
45
|
"dompurify": "^3.0.0",
|
|
46
46
|
"exceljs": "4.2.0",
|
|
47
47
|
"graphql-scalars": "^1.22.0",
|
|
48
48
|
"graphql-tag": "^2.12.6",
|
|
49
49
|
"graphql-upload": "^15.0.2",
|
|
50
|
+
"jszip": "^3.10.1",
|
|
50
51
|
"lit": "^3.1.2",
|
|
51
|
-
"marked": "^15.0.0"
|
|
52
|
+
"marked": "^15.0.0",
|
|
53
|
+
"pdfjs-dist": "5.7.284"
|
|
52
54
|
},
|
|
53
55
|
"browser": "dist-client/index.js",
|
|
54
56
|
"devDependencies": {
|
|
55
57
|
"copyfiles": "^2.4.1",
|
|
56
58
|
"rimraf": "^5.0.0"
|
|
57
59
|
},
|
|
58
|
-
"gitHead": "
|
|
60
|
+
"gitHead": "f66591e3864aab08a6fb93fb1fcf1c9de6b546ac"
|
|
59
61
|
}
|
|
@@ -58,7 +58,7 @@ class AssistantModeView {
|
|
|
58
58
|
|
|
59
59
|
@Field(() => GraphQLJSON, {
|
|
60
60
|
nullable: true,
|
|
61
|
-
description: "What this conversation accepts as attachments: { kinds: ('image'|'text'|'spreadsheet')[], maxBytes, maxFiles, maxChars }. Null: none."
|
|
61
|
+
description: "What this conversation accepts as attachments: { kinds: ('image'|'text'|'spreadsheet'|'document')[], maxBytes, maxFiles, maxChars }. Null: none."
|
|
62
62
|
})
|
|
63
63
|
attachments?: any
|
|
64
64
|
}
|
|
@@ -44,12 +44,12 @@ export function turnSystemPrompt(modePrompt: string, screen: { page?: string; su
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* The categories a mode opens: its own, the AI
|
|
47
|
+
* The categories a mode opens: its own, the AI 작업 패널 (design/ai/workspace.md), the attachment reader when it
|
|
48
48
|
* takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).
|
|
49
49
|
*/
|
|
50
50
|
export function modeToolCategories(mode: { toolCategories?: string[]; attachments?: { kinds?: string[] }; entryTargets?: string[]; readTargets?: string[] }): string[] | undefined {
|
|
51
51
|
const extra = [
|
|
52
|
-
/* Every conversation may put a large result on the AI
|
|
52
|
+
/* Every conversation may put a large result on the AI 작업 패널 (design/ai/workspace.md). */
|
|
53
53
|
'assistant-workspace',
|
|
54
54
|
/* Every conversation may read who works here and offer to bring one of them in (people/, ADR-0073 decision 3). */
|
|
55
55
|
...PEOPLE_CATEGORIES,
|
|
@@ -64,7 +64,7 @@ export function modeToolCategories(mode: { toolCategories?: string[]; attachment
|
|
|
64
64
|
/**
|
|
65
65
|
* The categories an unattended run of a mode opens: its own and the lists it reads (design/ai/unattended-run.md §3).
|
|
66
66
|
*
|
|
67
|
-
* Not the 입력안 tools - nobody is there to press the card - nor the AI
|
|
67
|
+
* Not the 입력안 tools - nobody is there to press the card - nor the AI 작업 패널 or the attachment reader: there is no
|
|
68
68
|
* conversation for a card or a file to belong to.
|
|
69
69
|
*/
|
|
70
70
|
export function unattendedToolCategories(mode: { toolCategories?: string[]; readTargets?: string[] }): string[] {
|
|
@@ -129,7 +129,7 @@ function axesOfKey(mode: FrontMode, key: string): string[] {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/**
|
|
132
|
-
* The categories a turn puts in front when the mode's tools do not fit in one call: the mode's own, the AI
|
|
132
|
+
* The categories a turn puts in front when the mode's tools do not fit in one call: the mode's own, the AI 작업 패널, the
|
|
133
133
|
* people list and the invite card, the attachment reader, and the open screen's records. The rest the model opens with `openTools`.
|
|
134
134
|
*/
|
|
135
135
|
export function modeFrontCategories(mode: FrontMode, screen: TurnScreen = {}): string[] {
|
|
@@ -72,7 +72,7 @@ describe('the open screen decides what goes in front', () => {
|
|
|
72
72
|
expect(modeFrontCategories({ ...mode, readTargets: [], entryTargets: [] }, { page: 'cash-transaction-list' })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'chat-attachments'])
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
-
it('no screen: the mode\'s own, the AI
|
|
75
|
+
it('no screen: the mode\'s own, the AI 작업 패널 and the attachment reader', () => {
|
|
76
76
|
expect(modeFrontCategories(mode)).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'chat-attachments'])
|
|
77
77
|
expect(modeFrontCategories(mode, { page: 'some-other-page' })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'chat-attachments'])
|
|
78
78
|
})
|
|
@@ -35,7 +35,7 @@ it('★ the settings come from the registered mode', () => {
|
|
|
35
35
|
expect(settings).toEqual({
|
|
36
36
|
/* The app's persona, then the rules every conversation follows (design/ai/screen-pattern.md §4). */
|
|
37
37
|
systemPrompt: conversationPrompt('you answer about one space'),
|
|
38
|
-
/* Every conversation may put a large result on the AI
|
|
38
|
+
/* Every conversation may put a large result on the AI 작업 패널 (design/ai/workspace.md). */
|
|
39
39
|
toolCategories: ['twin-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite'],
|
|
40
40
|
requireToolOnFirstTurn: true,
|
|
41
41
|
temperature: 0.2,
|
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/*
|
|
10
|
-
* 'spreadsheet'
|
|
11
|
-
*
|
|
10
|
+
* 'spreadsheet' and 'document' are kinds a mode opens, never files the door receives: the screen reads each .xlsx sheet
|
|
11
|
+
* into CSV text (client/utils/spreadsheet-text.ts), and each PDF, .docx and .pptx into text with its page and slide
|
|
12
|
+
* numbers (client/utils/document-text.ts) before sending, so a mode that takes them takes that text. A scanned PDF
|
|
13
|
+
* page arrives as an image, which the mode takes as 'image'.
|
|
12
14
|
*/
|
|
13
|
-
export type AttachmentKind = 'image' | 'text' | 'spreadsheet'
|
|
15
|
+
export type AttachmentKind = 'image' | 'text' | 'spreadsheet' | 'document'
|
|
14
16
|
|
|
15
17
|
export interface AttachmentPolicy {
|
|
16
18
|
kinds: AttachmentKind[]
|
|
@@ -47,9 +49,9 @@ export type AttachmentRefusal =
|
|
|
47
49
|
| { code: 'too-many-attachments'; maxFiles: number }
|
|
48
50
|
|
|
49
51
|
/** `null` when every file may go; otherwise the first reason, by name - said before sending, never cut silently. */
|
|
50
|
-
/** A kind the policy takes - text also when it takes spreadsheets, which arrive as
|
|
52
|
+
/** A kind the policy takes - text also when it takes spreadsheets or documents, which arrive as their text. */
|
|
51
53
|
function takes(policy: AttachmentPolicy, kind: AttachmentKind): boolean {
|
|
52
|
-
return policy.kinds.includes(kind) || (kind === 'text' && policy.kinds.includes('spreadsheet'))
|
|
54
|
+
return policy.kinds.includes(kind) || (kind === 'text' && (policy.kinds.includes('spreadsheet') || policy.kinds.includes('document')))
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
/** The characters of a text file the model reads under this policy. */
|
|
@@ -59,7 +59,7 @@ export interface EntryApplyDeps {
|
|
|
59
59
|
execute(document: string, variables: Record<string, unknown>): Promise<{ data?: any; errors?: readonly { message: string }[] }>
|
|
60
60
|
state: any
|
|
61
61
|
report?(message: string, error: unknown): void
|
|
62
|
-
/** The fingerprint of what the AI
|
|
62
|
+
/** The fingerprint of what the AI 작업 패널 showed, when the person reviewed it there - entered only if the proposal is still that one. */
|
|
63
63
|
reviewedHash?: string
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -101,7 +101,7 @@ export async function applyEntryProposal(key: string, deps: EntryApplyDeps, targ
|
|
|
101
101
|
if (proposal.kind !== 'entry') return nothing('refused', 'not-an-entry-proposal')
|
|
102
102
|
if (superseded) return nothing('refused', 'superseded')
|
|
103
103
|
if (deps.answered) return nothing('refused', 'already-answered')
|
|
104
|
-
/* What the
|
|
104
|
+
/* What the 작업 패널 showed and what is entered are one proposal (design/ai/workspace.md §6-1). */
|
|
105
105
|
if (deps.reviewedHash && deps.reviewedHash !== entryProposalHash(proposal)) return nothing('refused', 'review-stale')
|
|
106
106
|
|
|
107
107
|
const target = targetOf(proposal.target)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* One 입력안's fingerprint - what the AI
|
|
2
|
+
* One 입력안's fingerprint - what the AI 작업 패널 showed and what the card enters are the same proposal
|
|
3
3
|
* (operato-application/design/ai/workspace.md §6-1). The screen's copy and the server's are one rule;
|
|
4
4
|
* `test/entry-hash-agree.test.ts` runs both over one table.
|
|
5
5
|
*
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The application declares what it lets the assistant read: its list query, the conditions the model may set and
|
|
5
5
|
* how each becomes one of the query's filters, the columns to show, and the screen that lists the same records.
|
|
6
6
|
* The framework reads the rows through that query with the person's request state - the same @privilege and
|
|
7
|
-
* domain as the screen - counts and sums them itself, and puts a long result on the AI
|
|
7
|
+
* domain as the screen - counts and sums them itself, and puts a long result on the AI 작업 패널 as the query returned
|
|
8
8
|
* it. The model is told the count, the sums and a few rows; it never copies a table.
|
|
9
9
|
*
|
|
10
10
|
* Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.
|
|
@@ -101,7 +101,7 @@ export interface ReadTarget {
|
|
|
101
101
|
mine?: boolean
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
/** At most this many rows are read - the AI
|
|
104
|
+
/** At most this many rows are read - the AI 작업 패널 table's own limit (workspace-blocks MAX_TABLE_ROWS). */
|
|
105
105
|
export const MAX_READ_ROWS = 1000
|
|
106
106
|
/** Rows the model is shown; the rest stay on the card. */
|
|
107
107
|
export const ROWS_FOR_MODEL = 20
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The tools of an AI 조회 target (operato-application/design/ai/read-targets.md §4, §5).
|
|
3
3
|
*
|
|
4
4
|
* query<X> reads the rows through the application's list query as the person, and tells the model the count, the
|
|
5
|
-
* sums, the groups and a few rows. A longer result goes on the AI
|
|
5
|
+
* sums, the groups and a few rows. A longer result goes on the AI 작업 패널 as a proposal the server builds from the rows
|
|
6
6
|
* themselves - the table is never written by the model. open<X>Screen reads nothing: it hands the card the address
|
|
7
7
|
* of the application's list screen with the conditions it takes.
|
|
8
8
|
*/
|
|
@@ -145,7 +145,7 @@ export async function queryRecords(target: ReadTarget, args: any, context: any,
|
|
|
145
145
|
label: title,
|
|
146
146
|
...said,
|
|
147
147
|
forScreen: { workspace, contents: workspaceContents(workspace!) },
|
|
148
|
-
message: `${rows.length} rows of ${total} are on the AI
|
|
148
|
+
message: `${rows.length} rows of ${total} are on the AI 작업 패널 as read - the person opens it from the card.${incomplete} Say the point in one or two lines from the sums, groups and rows above; do not write the table out.`
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -186,14 +186,14 @@ export function readCategoryOf(target: ReadTarget, run: (context: any) => Graphq
|
|
|
186
186
|
doors: [door],
|
|
187
187
|
description:
|
|
188
188
|
`Read ${target.description} Set the conditions the question names. Returns the count, the sums, ` +
|
|
189
|
-
`groups when groupBy is given (a date column groups by month) and the first ${ROWS_FOR_MODEL} rows; more rows go on the AI
|
|
189
|
+
`groups when groupBy is given (a date column groups by month) and the first ${ROWS_FOR_MODEL} rows; more rows go on the AI 작업 패널 as read.`,
|
|
190
190
|
schema: {
|
|
191
191
|
type: 'object',
|
|
192
192
|
properties: {
|
|
193
193
|
id: { type: 'string', description: 'One record by its id, from an earlier answer - its detail.' },
|
|
194
194
|
...conditions,
|
|
195
195
|
groupBy: { type: 'string', description: `One column to group by: ${target.columns.map(one => one.name).join(', ')}.` },
|
|
196
|
-
show: { type: 'string', enum: ['answer', 'workspace'], description: 'workspace to put even a short result on the AI
|
|
196
|
+
show: { type: 'string', enum: ['answer', 'workspace'], description: 'workspace to put even a short result on the AI 작업 패널.' },
|
|
197
197
|
title: { type: 'string', description: 'The workspace title, in the person\'s language.' },
|
|
198
198
|
name: { type: 'string', description: 'The workspace key; the same key again makes a new version.' }
|
|
199
199
|
}
|
|
@@ -218,7 +218,7 @@ export function readCategoryOf(target: ReadTarget, run: (context: any) => Graphq
|
|
|
218
218
|
description: `data read: ${target.key}`,
|
|
219
219
|
guidance: [
|
|
220
220
|
`- To answer from ${target.key} records - a list, or one record found by its name, code or id - call ${queryToolName(target.key)} with the conditions the question names. Every number you say comes from its result - never add up rows yourself; use its sums and groups.`,
|
|
221
|
-
`- A list, a summary by item or month, or a comparison: call it with groupBy, and let a long result go on the AI
|
|
221
|
+
`- A list, a summary by item or month, or a comparison: call it with groupBy, and let a long result go on the AI 작업 패널 - then answer in one or two lines.`,
|
|
222
222
|
`- When the result says complete: false, say how many of how many were read and ask to narrow - never call partial sums the total.`,
|
|
223
223
|
...(target.columns.some(one => one.currency)
|
|
224
224
|
? [`- Sums of ${target.columns.filter(one => one.currency).map(one => one.name).join(', ')} come per currency. Say each currency's sum; never add two currencies into one number. A comparison across currencies uses a rate you read, and says the rate and its date.`]
|
|
@@ -84,7 +84,7 @@ describe('the read tool', () => {
|
|
|
84
84
|
expect(result.legend).toEqual({ kind: { HR: '인사', EVENT: '행사' }, state: { unread: '안 읽음' } })
|
|
85
85
|
})
|
|
86
86
|
|
|
87
|
-
it('★ the
|
|
87
|
+
it('★ the 작업 패널 table shows the words', async () => {
|
|
88
88
|
const result: any = await queryRecords(target, { show: 'workspace' }, context, run)
|
|
89
89
|
const table = result.forScreen.workspace.blocks.find((one: any) => one.kind === 'table')
|
|
90
90
|
expect(table.rows[0]).toMatchObject({ kind: '인사', state: '안 읽음', publishedAt: '2026-10-26 09:09' })
|
package/server/service/index.ts
CHANGED
|
@@ -40,7 +40,7 @@ export * from './data-read/read-target.js'
|
|
|
40
40
|
export { registerReadTarget, readCategoryOf, registerDetailRead, detailCategoryOf, runDetail, type DetailRun } from './data-read/read-tools.js'
|
|
41
41
|
export * from './data-read/list-read.js'
|
|
42
42
|
export * from './people/index.js'
|
|
43
|
-
/* The AI
|
|
43
|
+
/* The AI 작업 패널 - large results on a page of their own (design/ai/workspace.md). */
|
|
44
44
|
export * from './workspace/workspace-blocks.js'
|
|
45
45
|
/* 무엇이 근거인가 — 도구 결과 밖의 셋(프롬프트 · 대화 · 화면 스냅숏). */
|
|
46
46
|
export * from './grounding-sources.js'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* The people tools every conversation carries - the people list (people-read.ts) and the invite card (invite-tools.ts).
|
|
3
|
-
* Registered once, at boot, as the AI
|
|
3
|
+
* Registered once, at boot, as the AI 작업 패널 is.
|
|
4
4
|
*/
|
|
5
5
|
import { registerReadTarget } from '../data-read/read-tools.js'
|
|
6
6
|
import { peopleReadTarget } from './people-read.js'
|
|
@@ -197,7 +197,7 @@ describe('§3 what runs, and what is kept', () => {
|
|
|
197
197
|
expect(seen).toHaveLength(2)
|
|
198
198
|
})
|
|
199
199
|
|
|
200
|
-
it('★ only the mode\'s own reads are offered - no 입력안, no
|
|
200
|
+
it('★ only the mode\'s own reads are offered - no 입력안, no 작업 패널, and none behind a door the owner cannot pass', async () => {
|
|
201
201
|
const { client, seen } = scriptedModel([said('drafted')])
|
|
202
202
|
await runAssistantUnattended(input(), deps({ model: client }))
|
|
203
203
|
expect(seen[0].tools.map((tool: any) => tool.name).sort()).toEqual(['readCard', 'readGoals', 'readSales'])
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The AI
|
|
2
|
+
* The AI 작업 패널 - what a workspace is made of (operato-application/design/ai/workspace.md §3).
|
|
3
3
|
*
|
|
4
4
|
* The model does not write HTML. A workspace is a list of blocks of known kinds; the screen draws each, so
|
|
5
5
|
* theme, language and accessibility are the framework's. A block that does not fit is not dropped: it is
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* showInWorkspace - the model puts a large result on the AI
|
|
2
|
+
* showInWorkspace - the model puts a large result on the AI 작업 패널 (operato-application/design/ai/workspace.md §5).
|
|
3
3
|
*
|
|
4
4
|
* The result rides on the answer as a proposal of kind 'workspace', so the conversation's gates hold for it:
|
|
5
5
|
* who may read the conversation reads the workspace, and a value behind a door the reader cannot pass hides
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The AI
|
|
2
|
+
* The AI 작업 패널 (design/ai/workspace.md) - what a block may be, what happens to one that is not, and what the
|
|
3
3
|
* tool hands back.
|
|
4
4
|
*/
|
|
5
5
|
import { normalizeWorkspace, workspaceName, MAX_SERIES } from './workspace-blocks'
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
import * as server from '../server/service/chat-session/attachment-policy'
|
|
6
6
|
import * as screen from '../client/utils/attachment-policy'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/* A document arrives as its text (and a scanned page as an image) - the door sees what the screen sends, never the PDF. */
|
|
9
|
+
const policies = [undefined, { kinds: [] }, { kinds: ['image'] }, { kinds: ['image', 'text'], maxBytes: 100, maxFiles: 2 }, { kinds: ['document'] }, { kinds: ['image', 'document'] }] as any[]
|
|
9
10
|
const fileSets = [
|
|
10
11
|
[],
|
|
11
12
|
[{ name: 'a.png', type: 'image/png', size: 10 }],
|
|
12
13
|
[{ name: 'a.csv', type: 'text/csv', size: 10 }],
|
|
13
14
|
[{ name: 'report.CSV', type: '', size: 10 }],
|
|
14
15
|
[{ name: 'a.pdf', type: 'application/pdf', size: 10 }],
|
|
16
|
+
[{ name: 'a.pdf.txt', type: 'text/plain', size: 10 }],
|
|
17
|
+
[{ name: 'a.pdf - p2.jpg', type: 'image/jpeg', size: 10 }],
|
|
15
18
|
[{ name: 'big.png', type: 'image/png', size: 101 }],
|
|
16
19
|
[{ name: 'a.png', type: 'image/png', size: 1 }, { name: 'b.png', type: 'image/png', size: 1 }, { name: 'c.png', type: 'image/png', size: 1 }],
|
|
17
20
|
/* Mixed batches: the same "no" in the same order - kind, then size, then one too many, file by file. */
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A document read into the text a conversation carries (design/ai/shared-dock.md §9-6): every page and slide marked
|
|
3
|
+
* with its own number, a scanned PDF page drawn as an image, the old .doc/.ppt refused by name, and a document longer
|
|
4
|
+
* than the limit refused - never cut.
|
|
5
|
+
*/
|
|
6
|
+
import JSZip from 'jszip'
|
|
7
|
+
|
|
8
|
+
import * as screen from '../client/utils/attachment-policy'
|
|
9
|
+
import { docxText, documentKindOf, drawingText, isScannedPage, pdfPageText, readDocx, readPdf, readPptx, scannedPageName, slideOrder, xmlDecode } from '../client/utils/document-text'
|
|
10
|
+
|
|
11
|
+
const W = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
|
|
12
|
+
const para = (text: string, style?: string) => `<w:p>${style ? `<w:pPr><w:pStyle w:val="${style}"/></w:pPr>` : ''}<w:r><w:t xml:space="preserve">${text}</w:t></w:r></w:p>`
|
|
13
|
+
|
|
14
|
+
async function zip(parts: Record<string, string>): Promise<Uint8Array> {
|
|
15
|
+
const archive = new JSZip()
|
|
16
|
+
for (const [path, text] of Object.entries(parts)) archive.file(path, text)
|
|
17
|
+
return archive.generateAsync({ type: 'uint8array' })
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* A paragraph is a shape of its own; "#n" is the page-number placeholder a notes page repeats. */
|
|
21
|
+
const shape = (text: string) =>
|
|
22
|
+
text.startsWith('#')
|
|
23
|
+
? `<p:sp><p:nvSpPr><p:nvPr><p:ph type="sldNum" idx="5"/></p:nvPr></p:nvSpPr><p:txBody><a:p><a:fld type="slidenum"><a:t>${text.slice(1)}</a:t></a:fld></a:p></p:txBody></p:sp>`
|
|
24
|
+
: `<p:sp><p:txBody><a:p><a:r><a:t>${text}</a:t></a:r></a:p></p:txBody></p:sp>`
|
|
25
|
+
const slideXml = (...paragraphs: string[]) => `<p:sld xmlns:p="p" xmlns:a="a"><p:cSld><p:spTree>${paragraphs.map(shape).join('')}</p:spTree></p:cSld></p:sld>`
|
|
26
|
+
|
|
27
|
+
async function pptx(slides: { text: string[]; notes?: string[] }[], order = slides.map((_, i) => i)) {
|
|
28
|
+
const parts: Record<string, string> = {
|
|
29
|
+
'ppt/presentation.xml': `<p:presentation xmlns:p="p" xmlns:r="r"><p:sldIdLst>${order.map(i => `<p:sldId id="${256 + i}" r:id="rId${i + 1}"/>`).join('')}</p:sldIdLst></p:presentation>`,
|
|
30
|
+
'ppt/_rels/presentation.xml.rels': `<Relationships>${slides.map((_, i) => `<Relationship Id="rId${i + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide${i + 1}.xml"/>`).join('')}</Relationships>`
|
|
31
|
+
}
|
|
32
|
+
slides.forEach((slide, i) => {
|
|
33
|
+
parts[`ppt/slides/slide${i + 1}.xml`] = slideXml(...slide.text)
|
|
34
|
+
if (slide.notes) {
|
|
35
|
+
parts[`ppt/slides/_rels/slide${i + 1}.xml.rels`] = `<Relationships><Relationship Id="rId9" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" Target="../notesSlides/notesSlide${i + 1}.xml"/></Relationships>`
|
|
36
|
+
parts[`ppt/notesSlides/notesSlide${i + 1}.xml`] = slideXml(...slide.notes)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
return zip(parts)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pdf = (pages: string[]) => async () => ({
|
|
43
|
+
numPages: pages.length,
|
|
44
|
+
getPage: async (n: number) => ({
|
|
45
|
+
getTextContent: async () => ({ items: pages[n - 1].split('\n').map(str => ({ str, hasEOL: true })) })
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
const drawn = (n: number) => Promise.resolve(new Blob([`page ${n}`], { type: 'image/jpeg' }))
|
|
49
|
+
|
|
50
|
+
describe('which files are documents', () => {
|
|
51
|
+
it('by type, or by name when the browser did not know it', () => {
|
|
52
|
+
expect(documentKindOf({ name: 'a.PDF', type: '' })).toBe('pdf')
|
|
53
|
+
expect(documentKindOf({ name: 'x', type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' })).toBe('docx')
|
|
54
|
+
expect(documentKindOf({ name: '발표.pptx' })).toBe('pptx')
|
|
55
|
+
expect(documentKindOf({ name: '옛문서.doc' })).toBe('doc')
|
|
56
|
+
expect(documentKindOf({ name: '옛발표.ppt' })).toBe('ppt')
|
|
57
|
+
expect(documentKindOf({ name: 'a.png', type: 'image/png' })).toBeNull()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('★ the picker offers them only to a mode that takes documents', () => {
|
|
61
|
+
expect(screen.acceptOf({ kinds: ['image', 'text', 'document'] })).toMatch(/\.pdf/)
|
|
62
|
+
expect(screen.acceptOf({ kinds: ['image', 'text', 'document'] })).toMatch(/\.docx/)
|
|
63
|
+
expect(screen.acceptOf({ kinds: ['image', 'text', 'document'] })).toMatch(/\.pptx/)
|
|
64
|
+
expect(screen.acceptOf({ kinds: ['image', 'text'] })).not.toMatch(/\.pdf/)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('Word', () => {
|
|
69
|
+
it('★ paragraphs in order, headings led by #, a table row on one line', async () => {
|
|
70
|
+
const xml = `<w:document ${W}><w:body>${para('계약서', 'Title')}${para('제1조 목적', 'Heading2')}${para('갑과 을은 & 다음과 같이 정한다.')}<w:tbl><w:tr><w:tc>${para('품목')}</w:tc><w:tc>${para('금액')}</w:tc></w:tr><w:tr><w:tc>${para('설계')}${para('1차')}</w:tc><w:tc>${para('1,000,000')}</w:tc></w:tr></w:tbl>${para('끝')}</w:body></w:document>`
|
|
71
|
+
expect(docxText(xml)).toBe(['# 계약서', '## 제1조 목적', '갑과 을은 & 다음과 같이 정한다.', '| 품목 | 금액 |', '| 설계 1차 | 1,000,000 |', '끝'].join('\n'))
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('reads a .docx into one text file named after it', async () => {
|
|
75
|
+
const data = await zip({ 'word/document.xml': `<w:document ${W}><w:body>${para('본문')}</w:body></w:document>` })
|
|
76
|
+
const read: any = await readDocx(data, '계약서.docx', 20_000, JSZip)
|
|
77
|
+
expect(read.files.text.name).toBe('계약서.docx.txt')
|
|
78
|
+
expect(read.files.text.text).toMatch(/^# 계약서\.docx · Word/)
|
|
79
|
+
expect(read.files.text.text).toMatch(/\n본문\n$/)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('a file that is not a .docx, or one with nothing in it, is said', async () => {
|
|
83
|
+
expect(await readDocx(new Uint8Array([1, 2, 3]), 'x.docx', 20_000, JSZip)).toEqual({ refusal: { code: 'attachment-document-unreadable', name: 'x.docx' } })
|
|
84
|
+
const empty = await zip({ 'word/document.xml': `<w:document ${W}><w:body><w:p/></w:body></w:document>` })
|
|
85
|
+
expect(await readDocx(empty, 'empty.docx', 20_000, JSZip)).toEqual({ refusal: { code: 'attachment-document-empty', name: 'empty.docx' } })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('★ longer than the limit is refused, never cut', async () => {
|
|
89
|
+
const data = await zip({ 'word/document.xml': `<w:document ${W}><w:body>${para('가'.repeat(500))}</w:body></w:document>` })
|
|
90
|
+
expect(await readDocx(data, 'long.docx', 300, JSZip)).toEqual({ refusal: { code: 'attachment-document-too-long', name: 'long.docx', maxChars: 300 } })
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('PowerPoint', () => {
|
|
95
|
+
it('★ slides in the order the presentation shows them, each with its number and its notes', async () => {
|
|
96
|
+
const data = await pptx([{ text: ['둘째로 둔 슬라이드'] }, { text: ['표지', '2026 사업계획'], notes: ['발표자가 할 말', '#1'] }], [1, 0])
|
|
97
|
+
const read: any = await readPptx(data, '발표.pptx', 20_000, JSZip)
|
|
98
|
+
expect(read.files.text.text).toBe(
|
|
99
|
+
['# 발표.pptx · PowerPoint · 2 slides · each slide begins with "--- slide N ---", its notes after "notes:"', '--- slide 1 ---', '표지', '2026 사업계획', 'notes:', '발표자가 할 말', '--- slide 2 ---', '둘째로 둔 슬라이드', ''].join('\n')
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('reads the slide list through its relationships', () => {
|
|
104
|
+
expect(slideOrder('<p:sldIdLst><p:sldId id="1" r:id="rId7"/></p:sldIdLst>', '<Relationships><Relationship Id="rId7" Target="slides/slide3.xml"/></Relationships>')).toEqual(['ppt/slides/slide3.xml'])
|
|
105
|
+
expect(drawingText(slideXml('a', 'b'))).toEqual(['a', 'b'])
|
|
106
|
+
/* The number, date and footer a page repeats are left out by their placeholder, not by what they say. */
|
|
107
|
+
expect(drawingText(slideXml('a', '#7'))).toEqual(['a'])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('a deck with no words on any slide is said', async () => {
|
|
111
|
+
const data = await pptx([{ text: [] }])
|
|
112
|
+
expect(await readPptx(data, 'pictures.pptx', 20_000, JSZip)).toEqual({ refusal: { code: 'attachment-document-empty', name: 'pictures.pptx' } })
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('PDF', () => {
|
|
117
|
+
it('★ every page is marked with its own number', async () => {
|
|
118
|
+
const read: any = await readPdf(pdf(['제1조 이 계약은 갑과 을 사이의 용역에 관한 것이다', '제2조 계약 기간은 2026년 11월부터이다']), '계약서.pdf', 20_000, { allowed: true, max: 4 }, drawn)
|
|
119
|
+
expect(read.files.text.text).toBe(
|
|
120
|
+
['# 계약서.pdf · PDF · 2 pages · each page begins with "--- page N ---", N its own page number', '--- page 1 ---', '제1조 이 계약은 갑과 을 사이의 용역에 관한 것이다', '--- page 2 ---', '제2조 계약 기간은 2026년 11월부터이다', ''].join('\n')
|
|
121
|
+
)
|
|
122
|
+
expect(read.files.images).toEqual([])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('★ a page with no text is a scan - drawn as an image named by its page, and the text says where it went', async () => {
|
|
126
|
+
const read: any = await readPdf(pdf(['본문이 있는 첫 쪽입니다. 글자가 충분히 있습니다.', '3']), '스캔.pdf', 20_000, { allowed: true, max: 4 }, drawn)
|
|
127
|
+
expect(read.files.images.map((one: any) => [one.name, one.page])).toEqual([['스캔.pdf - p2.jpg', 2]])
|
|
128
|
+
expect(read.files.text.text).toContain(`--- page 2 ---\n(no text on this page - a scan, sent as the image "${scannedPageName('스캔.pdf', 2)}")`)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('★ a scan is refused by name when the mode takes no images, or when it has more pages than a message carries', async () => {
|
|
132
|
+
expect(await readPdf(pdf(['', '']), 'scan.pdf', 20_000, { allowed: false, max: 4 }, drawn)).toEqual({ refusal: { code: 'attachment-scan-needs-images', name: 'scan.pdf' } })
|
|
133
|
+
expect(await readPdf(pdf(['', '', '', '', '', '']), 'scan.pdf', 20_000, { allowed: true, max: 4 }, drawn)).toEqual({
|
|
134
|
+
refusal: { code: 'attachment-scan-too-many-pages', name: 'scan.pdf', pages: 6, max: 4 }
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('a wholly scanned PDF goes as its images and the page list', async () => {
|
|
139
|
+
const read: any = await readPdf(pdf(['', '']), 'scan.pdf', 20_000, { allowed: true, max: 4 }, drawn)
|
|
140
|
+
expect(read.files.images).toHaveLength(2)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('longer than the limit is refused, never cut; a broken file is said', async () => {
|
|
144
|
+
expect(await readPdf(pdf(['가'.repeat(400)]), 'long.pdf', 300, { allowed: true, max: 4 }, drawn)).toEqual({ refusal: { code: 'attachment-document-too-long', name: 'long.pdf', maxChars: 300 } })
|
|
145
|
+
expect(await readPdf(() => Promise.reject(new Error('bad')), 'bad.pdf', 300, { allowed: true, max: 4 }, drawn)).toEqual({ refusal: { code: 'attachment-document-unreadable', name: 'bad.pdf' } })
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('joins pdf.js items into lines and tells a scan by its letters', () => {
|
|
149
|
+
expect(pdfPageText([{ str: '제1조', hasEOL: false }, { str: ' 목적', hasEOL: true }, { str: '본문' }])).toBe('제1조 목적\n본문')
|
|
150
|
+
expect(isScannedPage('- 3 -')).toBe(true)
|
|
151
|
+
expect(isScannedPage('이 쪽에는 글자가 열여섯 자보다 많습니다')).toBe(false)
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('decodes the entities OOXML writes', () => {
|
|
156
|
+
expect(xmlDecode('<a> & "b" 가 가')).toBe('<a> & "b" 가 가')
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('every refusal is said in every locale', () => {
|
|
160
|
+
const { readFileSync } = require('fs')
|
|
161
|
+
const { join } = require('path')
|
|
162
|
+
const source = readFileSync(join(__dirname, '../client/utils/document-text.ts'), 'utf8')
|
|
163
|
+
const codes = [...new Set([...source.matchAll(/code: '(attachment-[a-z-]+)'/g)].map((match: any) => match[1]))]
|
|
164
|
+
|
|
165
|
+
it('the parser finds the refusals', () => {
|
|
166
|
+
expect(codes.length).toBeGreaterThanOrEqual(6)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
for (const locale of ['en', 'ko', 'ja', 'zh', 'ms', 'es']) {
|
|
170
|
+
it(locale, () => {
|
|
171
|
+
const dictionary = JSON.parse(readFileSync(join(__dirname, `../translations/${locale}.json`), 'utf8'))
|
|
172
|
+
expect(codes.filter(code => !dictionary[`ai-assistant.text.${code}`])).toEqual([])
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* Reviewing a 입력안 in the AI
|
|
2
|
+
* Reviewing a 입력안 in the AI 작업 패널 (operato-application/design/ai/workspace.md §6-1): read-only on the page,
|
|
3
3
|
* entered from the dock's card - and only as the page showed it.
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync } from 'fs'
|
package/test/workspace.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* The AI
|
|
2
|
+
* The AI 작업 패널 on the screen (design/ai/workspace.md) - which versions a reader sees, the card's version, and the
|
|
3
3
|
* table's sort and CSV. The page and the chat are read as text: importing them pulls the shell.
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync } from 'fs'
|
package/translations/en.json
CHANGED
|
@@ -293,6 +293,12 @@
|
|
|
293
293
|
"ai-assistant.text.pasted-table-name": "pasted table ({rows} rows)",
|
|
294
294
|
"ai-assistant.text.attachment-sheet-unreadable": "{name} cannot be read as an Excel file.",
|
|
295
295
|
"ai-assistant.text.attachment-sheet-empty": "{name} has no sheet with anything in it.",
|
|
296
|
+
"ai-assistant.text.attachment-old-office-not-read": "{name} is an old Office file (.doc or .ppt) and is not read. Save it as .docx or .pptx and attach it again.",
|
|
297
|
+
"ai-assistant.text.attachment-document-unreadable": "{name} cannot be read as a document.",
|
|
298
|
+
"ai-assistant.text.attachment-document-empty": "{name} has no text to read.",
|
|
299
|
+
"ai-assistant.text.attachment-document-too-long": "{name} is longer than {maxChars} characters as text and was not sent. Split it and attach the parts.",
|
|
300
|
+
"ai-assistant.text.attachment-scan-needs-images": "{name} has scanned pages, which go as images, and this conversation takes no images.",
|
|
301
|
+
"ai-assistant.text.attachment-scan-too-many-pages": "{name} has {pages} scanned pages; one message carries at most {max}. Attach the pages you need as a smaller PDF.",
|
|
296
302
|
"ai-assistant.entry.review-in-workspace": "review in the workspace",
|
|
297
303
|
"ai-assistant.entry.review-stale": "this proposal changed after it was reviewed in the workspace - review it again before entering.",
|
|
298
304
|
"ai-assistant.workspace.entry-not-found": "there is no proposal here that you can see.",
|
package/translations/es.json
CHANGED
|
@@ -293,6 +293,12 @@
|
|
|
293
293
|
"ai-assistant.text.pasted-table-name": "tabla pegada ({rows} filas)",
|
|
294
294
|
"ai-assistant.text.attachment-sheet-unreadable": "{name} no se puede leer como archivo de Excel.",
|
|
295
295
|
"ai-assistant.text.attachment-sheet-empty": "{name} no tiene ninguna hoja con contenido.",
|
|
296
|
+
"ai-assistant.text.attachment-old-office-not-read": "{name} es un archivo antiguo de Office (.doc o .ppt) y no se lee. Guárdalo como .docx o .pptx y adjúntalo de nuevo.",
|
|
297
|
+
"ai-assistant.text.attachment-document-unreadable": "{name} no se puede leer como documento.",
|
|
298
|
+
"ai-assistant.text.attachment-document-empty": "{name} no tiene texto que leer.",
|
|
299
|
+
"ai-assistant.text.attachment-document-too-long": "{name} supera los {maxChars} caracteres como texto y no se envió. Divídelo y adjunta las partes.",
|
|
300
|
+
"ai-assistant.text.attachment-scan-needs-images": "{name} tiene páginas escaneadas, que se envían como imágenes, y esta conversación no admite imágenes.",
|
|
301
|
+
"ai-assistant.text.attachment-scan-too-many-pages": "{name} tiene {pages} páginas escaneadas; un mensaje lleva como máximo {max}. Adjunta las páginas que necesites como un PDF más pequeño.",
|
|
296
302
|
"ai-assistant.entry.review-in-workspace": "revisar en el espacio de trabajo",
|
|
297
303
|
"ai-assistant.entry.review-stale": "esta propuesta cambió después de revisarse en el espacio de trabajo; revísela de nuevo antes de introducirla.",
|
|
298
304
|
"ai-assistant.workspace.entry-not-found": "aquí no hay ninguna propuesta que pueda ver.",
|
package/translations/ja.json
CHANGED
|
@@ -293,6 +293,12 @@
|
|
|
293
293
|
"ai-assistant.text.pasted-table-name": "貼り付けた表 {rows}行",
|
|
294
294
|
"ai-assistant.text.attachment-sheet-unreadable": "{name} を Excel ファイルとして読み込めません。",
|
|
295
295
|
"ai-assistant.text.attachment-sheet-empty": "{name} には内容のあるシートがありません。",
|
|
296
|
+
"ai-assistant.text.attachment-old-office-not-read": "{name} は古い Office 形式(.doc・.ppt)のため読み込みません。.docx・.pptx で保存して再度添付してください。",
|
|
297
|
+
"ai-assistant.text.attachment-document-unreadable": "{name} を文書として読み込めません。",
|
|
298
|
+
"ai-assistant.text.attachment-document-empty": "{name} に読み取れる文字がありません。",
|
|
299
|
+
"ai-assistant.text.attachment-document-too-long": "{name} はテキストにすると {maxChars} 文字を超えるため送信しませんでした。分けて添付してください。",
|
|
300
|
+
"ai-assistant.text.attachment-scan-needs-images": "{name} にはスキャンしたページがあり画像として送る必要がありますが、この会話は画像を受け付けません。",
|
|
301
|
+
"ai-assistant.text.attachment-scan-too-many-pages": "{name} のスキャンしたページは {pages} ページですが、一度に送れるのは {max} ページまでです。必要なページだけを PDF に分けて添付してください。",
|
|
296
302
|
"ai-assistant.entry.review-in-workspace": "作業ボードで確認",
|
|
297
303
|
"ai-assistant.entry.review-stale": "作業ボードで確認した後に入力案が変わりました。もう一度確認してから入力してください。",
|
|
298
304
|
"ai-assistant.workspace.entry-not-found": "表示できる入力案はありません。",
|
package/translations/ko.json
CHANGED
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
"ai-assistant.entry.issue.not-in-source": "자료에 없는 값입니다",
|
|
224
224
|
"ai-assistant.entry.issue.source-scattered": "값이 자료의 한 줄에 함께 있지 않습니다",
|
|
225
225
|
"ai-assistant.entry.issue.source-not-checked": "사진 자료는 대조하지 못했습니다",
|
|
226
|
-
"ai-assistant.workspace.title": "AI
|
|
226
|
+
"ai-assistant.workspace.title": "AI 작업 패널",
|
|
227
227
|
"ai-assistant.workspace.unreadable": "그릴 수 없는 블록입니다 ({reason})",
|
|
228
228
|
"ai-assistant.workspace.rows": "{count}행",
|
|
229
229
|
"ai-assistant.workspace.csv": "CSV",
|
|
@@ -231,14 +231,14 @@
|
|
|
231
231
|
"ai-assistant.workspace.other": "기타",
|
|
232
232
|
"ai-assistant.workspace.as-chart": "차트",
|
|
233
233
|
"ai-assistant.workspace.as-table": "표로 보기",
|
|
234
|
-
"ai-assistant.workspace.loading": "
|
|
235
|
-
"ai-assistant.workspace.unreadable-page": "
|
|
236
|
-
"ai-assistant.workspace.not-found": "볼 수 있는
|
|
237
|
-
"ai-assistant.workspace.newer": "새
|
|
234
|
+
"ai-assistant.workspace.loading": "작업 패널을 불러오는 중…",
|
|
235
|
+
"ai-assistant.workspace.unreadable-page": "작업 패널을 읽지 못했습니다: {reason}",
|
|
236
|
+
"ai-assistant.workspace.not-found": "볼 수 있는 작업 패널이 없습니다.",
|
|
237
|
+
"ai-assistant.workspace.newer": "새 버전({version})이 있습니다.",
|
|
238
238
|
"ai-assistant.workspace.show-newer": "보기",
|
|
239
|
-
"ai-assistant.workspace.versions": "
|
|
240
|
-
"ai-assistant.workspace.version": "
|
|
241
|
-
"ai-assistant.workspace.open": "
|
|
239
|
+
"ai-assistant.workspace.versions": "버전",
|
|
240
|
+
"ai-assistant.workspace.version": "버전 {version} / {of}",
|
|
241
|
+
"ai-assistant.workspace.open": "작업 패널에서 보기",
|
|
242
242
|
"ai-assistant.screen.open": "화면에서 열기",
|
|
243
243
|
"ai-assistant.screen.not-applied": "화면에서는 걸리지 않는 조건: {conditions}",
|
|
244
244
|
"ai-assistant.read.count": "건수",
|
|
@@ -293,8 +293,14 @@
|
|
|
293
293
|
"ai-assistant.text.pasted-table-name": "붙여넣은 표 {rows}행",
|
|
294
294
|
"ai-assistant.text.attachment-sheet-unreadable": "{name} 을 엑셀 파일로 읽을 수 없습니다.",
|
|
295
295
|
"ai-assistant.text.attachment-sheet-empty": "{name} 에 내용이 있는 시트가 없습니다.",
|
|
296
|
-
"ai-assistant.
|
|
297
|
-
"ai-assistant.
|
|
296
|
+
"ai-assistant.text.attachment-old-office-not-read": "{name} 은 옛 Office 형식(.doc·.ppt)이라 읽지 않습니다. .docx·.pptx 로 저장해 다시 첨부해 주십시오.",
|
|
297
|
+
"ai-assistant.text.attachment-document-unreadable": "{name} 을 문서로 읽을 수 없습니다.",
|
|
298
|
+
"ai-assistant.text.attachment-document-empty": "{name} 에 읽을 글자가 없습니다.",
|
|
299
|
+
"ai-assistant.text.attachment-document-too-long": "{name} 은 글자로 바꾸면 {maxChars} 자를 넘어 보내지 않았습니다. 나눠 첨부해 주십시오.",
|
|
300
|
+
"ai-assistant.text.attachment-scan-needs-images": "{name} 에 스캔한 쪽이 있어 이미지로 보내야 하는데, 이 대화는 이미지를 받지 않습니다.",
|
|
301
|
+
"ai-assistant.text.attachment-scan-too-many-pages": "{name} 의 스캔한 쪽은 {pages}쪽인데 한 번에 {max}쪽까지 보낼 수 있습니다. 필요한 쪽만 PDF 로 나눠 첨부해 주십시오.",
|
|
302
|
+
"ai-assistant.entry.review-in-workspace": "작업 패널에서 검토",
|
|
303
|
+
"ai-assistant.entry.review-stale": "작업 패널에서 검토한 뒤 입력안이 바뀌었습니다. 다시 검토한 뒤 입력하십시오.",
|
|
298
304
|
"ai-assistant.workspace.entry-not-found": "볼 수 있는 입력안이 없습니다.",
|
|
299
305
|
"ai-assistant.workspace.entry-superseded": "대화에서 더 새로운 카드가 이 카드를 대신했습니다. 그 카드를 검토하십시오.",
|
|
300
306
|
"ai-assistant.workspace.entry-press-in-dock": "검토 화면입니다. 입력은 AI 도크의 카드에 있는 단추로 합니다.",
|