@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type FileUploadLimits } from '@operato/utils/file-acceptance.js';
|
|
2
|
-
export type AttachmentKind = 'image' | 'text' | 'spreadsheet';
|
|
2
|
+
export type AttachmentKind = 'image' | 'text' | 'spreadsheet' | 'document';
|
|
3
3
|
export interface AttachmentPolicy {
|
|
4
4
|
kinds: AttachmentKind[];
|
|
5
5
|
maxBytes?: number;
|
|
@@ -39,7 +39,7 @@ export declare function refuseAttachments(policy: AttachmentPolicy | null | unde
|
|
|
39
39
|
type?: string;
|
|
40
40
|
size?: number;
|
|
41
41
|
}[], server?: FileUploadLimits): AttachmentRefusal | null;
|
|
42
|
-
/** The file picker's `accept` for a policy - a spreadsheet as the person picks it (.xlsx). */
|
|
42
|
+
/** The file picker's `accept` for a policy - a spreadsheet or a document as the person picks it (.xlsx, .pdf, ...). */
|
|
43
43
|
export declare function acceptOf(policy: AttachmentPolicy | null | undefined): string;
|
|
44
44
|
/** Where a conversation's file opens - the gated route, under the page's domain path. */
|
|
45
45
|
export declare function chatAttachmentUrl(sessionId: string, attachmentId: string): string;
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* server's upload limits. What stays here is the mode's words: kinds, and a spreadsheet read into text.
|
|
11
11
|
*/
|
|
12
12
|
import { narrowLimits, sortPickedFiles } from '@operato/utils/file-acceptance.js';
|
|
13
|
+
import { DOCUMENT_ACCEPT } from './document-text.js';
|
|
13
14
|
export const DEFAULT_MAX_BYTES = 5 * 1024 * 1024;
|
|
14
15
|
export const DEFAULT_MAX_FILES = 5;
|
|
15
16
|
/** How much of a text file reaches the model - the server's TEXT_CHARS_FOR_MODEL. */
|
|
@@ -21,8 +22,11 @@ const XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.s
|
|
|
21
22
|
const KIND_ACCEPT = {
|
|
22
23
|
image: ['image/*'],
|
|
23
24
|
text: [...TEXT_EXTENSIONS, ...TEXT_TYPES],
|
|
24
|
-
spreadsheet: ['.xlsx', XLSX_TYPE]
|
|
25
|
+
spreadsheet: ['.xlsx', XLSX_TYPE],
|
|
26
|
+
document: DOCUMENT_ACCEPT
|
|
25
27
|
};
|
|
28
|
+
/** The kinds that are read into text on the screen - they arrive as text. */
|
|
29
|
+
const READ_INTO_TEXT = ['spreadsheet', 'document'];
|
|
26
30
|
/** A kind the policy takes - text also when it takes spreadsheets, which arrive as the text of their sheets. */
|
|
27
31
|
const acceptFor = (kinds) => [...new Set(kinds.flatMap(kind => KIND_ACCEPT[kind]))].join(',');
|
|
28
32
|
/** The characters of a text file the model reads under this policy. */
|
|
@@ -43,8 +47,8 @@ export function refuseAttachments(policy, files, server) {
|
|
|
43
47
|
return null;
|
|
44
48
|
if (!policy || !policy.kinds?.length)
|
|
45
49
|
return { code: 'attachments-not-accepted' };
|
|
46
|
-
/* What is sent: a spreadsheet
|
|
47
|
-
const sent = policy.kinds.map(kind => (kind
|
|
50
|
+
/* What is sent: a spreadsheet or a document arrives as its text. */
|
|
51
|
+
const sent = policy.kinds.map(kind => (READ_INTO_TEXT.includes(kind) ? 'text' : kind));
|
|
48
52
|
const limits = narrowLimits(limitsOf(policy), server);
|
|
49
53
|
const { refused } = sortPickedFiles(files, { accept: acceptFor(sent), limits });
|
|
50
54
|
if (!refused.length)
|
|
@@ -56,10 +60,10 @@ export function refuseAttachments(policy, files, server) {
|
|
|
56
60
|
? { code: 'attachment-kind-not-accepted', name: first.file.name || '', kinds: policy.kinds }
|
|
57
61
|
: { code: 'attachment-too-large', name: first.file.name || '', maxBytes: limits.maxFileSize };
|
|
58
62
|
}
|
|
59
|
-
/** The file picker's `accept` for a policy - a spreadsheet as the person picks it (.xlsx). */
|
|
63
|
+
/** The file picker's `accept` for a policy - a spreadsheet or a document as the person picks it (.xlsx, .pdf, ...). */
|
|
60
64
|
export function acceptOf(policy) {
|
|
61
65
|
const kinds = policy?.kinds ?? [];
|
|
62
|
-
return acceptFor(kinds.includes(
|
|
66
|
+
return acceptFor(kinds.some(kind => READ_INTO_TEXT.includes(kind)) ? [...kinds, 'text'] : kinds);
|
|
63
67
|
}
|
|
64
68
|
/** Where a conversation's file opens - the gated route, under the page's domain path. */
|
|
65
69
|
export function chatAttachmentUrl(sessionId, attachmentId) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attachment-policy.js","sourceRoot":"","sources":["../../client/utils/attachment-policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,YAAY,EAAE,eAAe,EAAyB,MAAM,mCAAmC,CAAA;
|
|
1
|
+
{"version":3,"file":"attachment-policy.js","sourceRoot":"","sources":["../../client/utils/attachment-policy.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * What a conversation accepts as attachments - the screen's side of the server rule\n * (server/service/chat-session/attachment-policy.ts, design/ai/shared-dock.md §9-1).\n *\n * The screen checks first so the person hears \"no\" before anything is sent; the door checks again and is\n * the authority. `test/attachment-policy-agree.test.ts` runs both over one table so they cannot drift.\n *\n * The screen's check is the one file-picking rule (@operato/utils `sortPickedFiles`, ADR-0061 2026-10-17): the mode's\n * kinds become an `accept` (the table below, the one place), its size and count become limits, narrowed by the\n * server's upload limits. What stays here is the mode's words: kinds, and a spreadsheet read into text.\n */\nimport { narrowLimits, sortPickedFiles, type FileUploadLimits } from '@operato/utils/file-acceptance.js'\n\nimport { DOCUMENT_ACCEPT } from './document-text.js'\n\n/*\n * 'spreadsheet' and 'document' are kinds a mode opens, never files the door receives: the screen reads each .xlsx\n * sheet into CSV text (spreadsheet-text.ts), and each PDF, .docx and .pptx into text (document-text.ts) before\n * sending, so a mode that takes them takes that text - and a scanned PDF page as an image.\n */\nexport type AttachmentKind = 'image' | 'text' | 'spreadsheet' | 'document'\n\nexport interface AttachmentPolicy {\n kinds: AttachmentKind[]\n maxBytes?: number\n maxFiles?: number\n /** Per text file, the characters the model reads - a sheet read into text longer than this is refused, not cut. */\n maxChars?: number\n}\n\nexport const DEFAULT_MAX_BYTES = 5 * 1024 * 1024\nexport const DEFAULT_MAX_FILES = 5\n/** How much of a text file reaches the model - the server's TEXT_CHARS_FOR_MODEL. */\nexport const TEXT_CHARS_FOR_MODEL = 20_000\n\nconst TEXT_TYPES = ['text/plain', 'text/csv', 'text/markdown', 'application/json']\nconst TEXT_EXTENSIONS = ['.txt', '.csv', '.md', '.json']\nconst XLSX_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n\n/** Each kind as `accept` patterns - the one table from the mode's kinds to what a picker takes. */\nconst KIND_ACCEPT: Record<AttachmentKind, string[]> = {\n image: ['image/*'],\n text: [...TEXT_EXTENSIONS, ...TEXT_TYPES],\n spreadsheet: ['.xlsx', XLSX_TYPE],\n document: DOCUMENT_ACCEPT\n}\n\n/** The kinds that are read into text on the screen - they arrive as text. */\nconst READ_INTO_TEXT: AttachmentKind[] = ['spreadsheet', 'document']\n\n/** A kind the policy takes - text also when it takes spreadsheets, which arrive as the text of their sheets. */\nconst acceptFor = (kinds: AttachmentKind[]) => [...new Set(kinds.flatMap(kind => KIND_ACCEPT[kind]))].join(',')\n\nexport type AttachmentRefusal =\n | { code: 'attachments-not-accepted' }\n | { code: 'attachment-kind-not-accepted'; name: string; kinds: AttachmentKind[] }\n | { code: 'attachment-too-large'; name: string; maxBytes: number }\n | { code: 'too-many-attachments'; maxFiles: number }\n\n/** The characters of a text file the model reads under this policy. */\nexport function textCharsOf(policy: AttachmentPolicy | null | undefined): number {\n return policy?.maxChars ?? TEXT_CHARS_FOR_MODEL\n}\n\n/** The mode's size and count as picker limits. */\nexport function limitsOf(policy: AttachmentPolicy | null | undefined): FileUploadLimits {\n return { maxFileSize: policy?.maxBytes ?? DEFAULT_MAX_BYTES, maxFiles: policy?.maxFiles ?? DEFAULT_MAX_FILES }\n}\n\n/**\n * Whether these files may go, by the one picking rule. `server` is the server's upload limit (shell\n * `fileUploadLimits`); the tighter of it and the mode's policy applies. The first reason, by name: file by file, its\n * kind, then its size, then whether it is one too many - the door says them in the same order.\n */\nexport function refuseAttachments(\n policy: AttachmentPolicy | null | undefined,\n files: { name?: string; type?: string; size?: number }[],\n server?: FileUploadLimits\n): AttachmentRefusal | null {\n if (!files.length) return null\n if (!policy || !policy.kinds?.length) return { code: 'attachments-not-accepted' }\n\n /* What is sent: a spreadsheet or a document arrives as its text. */\n const sent = policy.kinds.map(kind => (READ_INTO_TEXT.includes(kind) ? 'text' : kind))\n const limits = narrowLimits(limitsOf(policy), server)\n const { refused } = sortPickedFiles(files, { accept: acceptFor(sent), limits })\n if (!refused.length) return null\n\n const [first] = refused\n if (first.reason === 'count') return { code: 'too-many-attachments', maxFiles: limits.maxFiles! }\n return first.reason === 'type'\n ? { code: 'attachment-kind-not-accepted', name: first.file.name || '', kinds: policy.kinds }\n : { code: 'attachment-too-large', name: first.file.name || '', maxBytes: limits.maxFileSize! }\n}\n\n/** The file picker's `accept` for a policy - a spreadsheet or a document as the person picks it (.xlsx, .pdf, ...). */\nexport function acceptOf(policy: AttachmentPolicy | null | undefined): string {\n const kinds = policy?.kinds ?? []\n return acceptFor(kinds.some(kind => READ_INTO_TEXT.includes(kind)) ? [...kinds, 'text'] : kinds)\n}\n\n/** Where a conversation's file opens - the gated route, under the page's domain path. */\nexport function chatAttachmentUrl(sessionId: string, attachmentId: string): string {\n const domainPath = /^\\/domain\\/[^/]+/.exec(location.pathname)?.[0] ?? ''\n return `${domainPath}/chat-attachments/${encodeURIComponent(sessionId)}/${encodeURIComponent(attachmentId)}`\n}\n"]}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export type DocumentKind = 'pdf' | 'docx' | 'pptx' | 'doc' | 'ppt';
|
|
2
|
+
/** The picker's `accept` for documents. */
|
|
3
|
+
export declare const DOCUMENT_ACCEPT: string[];
|
|
4
|
+
/** A document by its type, or by its name when the browser did not know the type. */
|
|
5
|
+
export declare function documentKindOf(file: {
|
|
6
|
+
type?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
}): DocumentKind | null;
|
|
9
|
+
export type DocumentRefusal = {
|
|
10
|
+
code: 'attachment-old-office-not-read';
|
|
11
|
+
name: string;
|
|
12
|
+
} | {
|
|
13
|
+
code: 'attachment-document-unreadable';
|
|
14
|
+
name: string;
|
|
15
|
+
} | {
|
|
16
|
+
code: 'attachment-document-empty';
|
|
17
|
+
name: string;
|
|
18
|
+
} | {
|
|
19
|
+
code: 'attachment-document-too-long';
|
|
20
|
+
name: string;
|
|
21
|
+
maxChars: number;
|
|
22
|
+
} | {
|
|
23
|
+
code: 'attachment-scan-needs-images';
|
|
24
|
+
name: string;
|
|
25
|
+
} | {
|
|
26
|
+
code: 'attachment-scan-too-many-pages';
|
|
27
|
+
name: string;
|
|
28
|
+
pages: number;
|
|
29
|
+
max: number;
|
|
30
|
+
};
|
|
31
|
+
/** What a document becomes: one text file, and the scanned pages of a PDF as images. */
|
|
32
|
+
export interface DocumentFiles {
|
|
33
|
+
text: {
|
|
34
|
+
name: string;
|
|
35
|
+
text: string;
|
|
36
|
+
};
|
|
37
|
+
images: {
|
|
38
|
+
name: string;
|
|
39
|
+
page: number;
|
|
40
|
+
blob: Blob;
|
|
41
|
+
}[];
|
|
42
|
+
}
|
|
43
|
+
/** The file name the text travels under - "계약서.pdf" → "계약서.pdf.txt", so the source stays readable. */
|
|
44
|
+
export declare const documentTextName: (fileName: string) => string;
|
|
45
|
+
/** The file name a scanned page travels under - "계약서.pdf" page 3 → "계약서.pdf - p3.jpg". */
|
|
46
|
+
export declare const scannedPageName: (fileName: string, page: number) => string;
|
|
47
|
+
export interface XmlToken {
|
|
48
|
+
kind: 'open' | 'close' | 'empty' | 'text';
|
|
49
|
+
name?: string;
|
|
50
|
+
attrs?: string;
|
|
51
|
+
text?: string;
|
|
52
|
+
}
|
|
53
|
+
export declare function xmlDecode(text: string): string;
|
|
54
|
+
/** Tags and text in document order. Comments, declarations and processing instructions are skipped. */
|
|
55
|
+
export declare function xmlTokens(xml: string): Generator<XmlToken>;
|
|
56
|
+
/**
|
|
57
|
+
* A Word body as text: paragraphs in order, a heading led by "#" for its level, a table as one line per row with its
|
|
58
|
+
* cells between " | ". Text inside a table cell is joined with spaces, so a row stays one line.
|
|
59
|
+
*/
|
|
60
|
+
export declare function docxText(xml: string): string;
|
|
61
|
+
/** The text of one slide or notes part: each paragraph on its own line, the repeated placeholders left out. */
|
|
62
|
+
export declare function drawingText(xml: string): string[];
|
|
63
|
+
/** The slides in the order the presentation shows them - its slide list read through its relationships. */
|
|
64
|
+
export declare function slideOrder(presentationXml: string, relsXml: string): string[];
|
|
65
|
+
/** The notes part a slide points at, if it has one. */
|
|
66
|
+
export declare function notesTarget(slidePath: string, slideRelsXml: string): string | null;
|
|
67
|
+
/** One slide as text: its paragraphs, then its speaker notes. */
|
|
68
|
+
export declare function slideText(number: number, slide: string[], notes: string[]): string;
|
|
69
|
+
/** One PDF page's text items as lines - pdf.js marks the end of a line on the item that ends it. */
|
|
70
|
+
export declare function pdfPageText(items: {
|
|
71
|
+
str?: string;
|
|
72
|
+
hasEOL?: boolean;
|
|
73
|
+
}[]): string;
|
|
74
|
+
/** A page with fewer letters than this is a scan - its text is a page number or a stray mark, not its content. */
|
|
75
|
+
export declare const SCANNED_PAGE_LETTERS = 16;
|
|
76
|
+
export declare const isScannedPage: (text: string) => boolean;
|
|
77
|
+
/** The header line of a document's text - what it was and how it is marked. */
|
|
78
|
+
export declare function documentHeader(fileName: string, kind: 'pdf' | 'docx' | 'pptx', count?: number): string;
|
|
79
|
+
/** What reads a zip - JSZip, passed in so the screen loads it only when a document is attached. */
|
|
80
|
+
export interface ZipReader {
|
|
81
|
+
loadAsync(data: ArrayBuffer | Uint8Array): Promise<{
|
|
82
|
+
file(path: string): {
|
|
83
|
+
async(type: 'string'): Promise<string>;
|
|
84
|
+
} | null;
|
|
85
|
+
}>;
|
|
86
|
+
}
|
|
87
|
+
export declare function readDocx(data: ArrayBuffer | Uint8Array, fileName: string, maxChars: number, zip: ZipReader): Promise<{
|
|
88
|
+
files: DocumentFiles;
|
|
89
|
+
} | {
|
|
90
|
+
refusal: DocumentRefusal;
|
|
91
|
+
}>;
|
|
92
|
+
export declare function readPptx(data: ArrayBuffer | Uint8Array, fileName: string, maxChars: number, zip: ZipReader): Promise<{
|
|
93
|
+
files: DocumentFiles;
|
|
94
|
+
} | {
|
|
95
|
+
refusal: DocumentRefusal;
|
|
96
|
+
}>;
|
|
97
|
+
/** What reads a PDF - pdf.js's document, narrowed to what is used, so a test can hand in pages. */
|
|
98
|
+
export interface PdfSource {
|
|
99
|
+
numPages: number;
|
|
100
|
+
getPage(number: number): Promise<{
|
|
101
|
+
getTextContent(): Promise<{
|
|
102
|
+
items: any[];
|
|
103
|
+
}>;
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Every page of a PDF as text, its scanned pages drawn as images by `drawPage` (the screen's canvas). A scan cannot be
|
|
108
|
+
* read unless the mode takes images, and no more scanned pages are drawn than the message may carry - both refused by
|
|
109
|
+
* name, never dropped.
|
|
110
|
+
*/
|
|
111
|
+
export declare function readPdf(open: () => Promise<PdfSource>, fileName: string, maxChars: number, scans: {
|
|
112
|
+
allowed: boolean;
|
|
113
|
+
max: number;
|
|
114
|
+
}, drawPage: (number: number) => Promise<Blob>): Promise<{
|
|
115
|
+
files: DocumentFiles;
|
|
116
|
+
} | {
|
|
117
|
+
refusal: DocumentRefusal;
|
|
118
|
+
}>;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A document read into text the conversation can carry (design/ai/shared-dock.md §9-6) - PDF, Word (.docx) and
|
|
3
|
+
* PowerPoint (.pptx), as a spreadsheet is (spreadsheet-text.ts): the door takes images and text only, so a mode that
|
|
4
|
+
* opens 'document' has the screen read the file before sending (2026-10-28, the user: 「pdf, word, ppt 파일은 처리할
|
|
5
|
+
* 수 없는가?」).
|
|
6
|
+
*
|
|
7
|
+
* - Every page and slide is marked with its own number, so an answer can say "page 3" or "slide 5" and be checked.
|
|
8
|
+
* - A PDF page with no text on it is a scan: it is drawn into an image and sent beside the text, named by its page.
|
|
9
|
+
* - The old binary .doc and .ppt are refused by name, as .xls is - nothing here opens them.
|
|
10
|
+
* - The limit is on the text after reading. A document over it is refused, never cut: a cut contract is read as the
|
|
11
|
+
* whole contract.
|
|
12
|
+
* - Word keeps no pages (they are laid out when printed), so a .docx is read in order with its headings marked.
|
|
13
|
+
*/
|
|
14
|
+
const PDF_TYPE = 'application/pdf';
|
|
15
|
+
const DOCX_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
16
|
+
const PPTX_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
|
17
|
+
const DOC_TYPE = 'application/msword';
|
|
18
|
+
const PPT_TYPE = 'application/vnd.ms-powerpoint';
|
|
19
|
+
/** The picker's `accept` for documents. */
|
|
20
|
+
export const DOCUMENT_ACCEPT = ['.pdf', PDF_TYPE, '.docx', DOCX_TYPE, '.pptx', PPTX_TYPE];
|
|
21
|
+
/** A document by its type, or by its name when the browser did not know the type. */
|
|
22
|
+
export function documentKindOf(file) {
|
|
23
|
+
const type = (file.type || '').toLowerCase();
|
|
24
|
+
const name = (file.name || '').toLowerCase();
|
|
25
|
+
if (type === PDF_TYPE || name.endsWith('.pdf'))
|
|
26
|
+
return 'pdf';
|
|
27
|
+
if (type === DOCX_TYPE || name.endsWith('.docx'))
|
|
28
|
+
return 'docx';
|
|
29
|
+
if (type === PPTX_TYPE || name.endsWith('.pptx'))
|
|
30
|
+
return 'pptx';
|
|
31
|
+
if (type === DOC_TYPE || name.endsWith('.doc'))
|
|
32
|
+
return 'doc';
|
|
33
|
+
if (type === PPT_TYPE || name.endsWith('.ppt'))
|
|
34
|
+
return 'ppt';
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/** The file name the text travels under - "계약서.pdf" → "계약서.pdf.txt", so the source stays readable. */
|
|
38
|
+
export const documentTextName = (fileName) => `${fileName}.txt`;
|
|
39
|
+
/** The file name a scanned page travels under - "계약서.pdf" page 3 → "계약서.pdf - p3.jpg". */
|
|
40
|
+
export const scannedPageName = (fileName, page) => `${fileName} - p${page}.jpg`;
|
|
41
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
|
|
42
|
+
export function xmlDecode(text) {
|
|
43
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (whole, code) => {
|
|
44
|
+
if (code[0] === '#') {
|
|
45
|
+
const n = code[1] === 'x' || code[1] === 'X' ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
|
|
46
|
+
return Number.isFinite(n) ? String.fromCodePoint(n) : whole;
|
|
47
|
+
}
|
|
48
|
+
return ENTITIES[code.toLowerCase()] ?? whole;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Tags and text in document order. Comments, declarations and processing instructions are skipped. */
|
|
52
|
+
export function* xmlTokens(xml) {
|
|
53
|
+
const pattern = /<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<!\[CDATA\[([\s\S]*?)\]\]>|<!(?:[^>]*)>|<(\/?)([\w:.-]+)([^>]*?)(\/?)>|([^<]+)/g;
|
|
54
|
+
for (const match of xml.matchAll(pattern)) {
|
|
55
|
+
const [, cdata, slash, name, attrs, selfClose, text] = match;
|
|
56
|
+
if (cdata !== undefined)
|
|
57
|
+
yield { kind: 'text', text: cdata };
|
|
58
|
+
else if (name)
|
|
59
|
+
yield { kind: slash ? 'close' : selfClose ? 'empty' : 'open', name, attrs };
|
|
60
|
+
else if (text !== undefined)
|
|
61
|
+
yield { kind: 'text', text: xmlDecode(text) };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const attrOf = (attrs, name) => new RegExp(`\\b${name}="([^"]*)"`).exec(attrs ?? '')?.[1];
|
|
65
|
+
/**
|
|
66
|
+
* A Word body as text: paragraphs in order, a heading led by "#" for its level, a table as one line per row with its
|
|
67
|
+
* cells between " | ". Text inside a table cell is joined with spaces, so a row stays one line.
|
|
68
|
+
*/
|
|
69
|
+
export function docxText(xml) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
let paragraph = '';
|
|
72
|
+
let heading = 0;
|
|
73
|
+
let inText = false;
|
|
74
|
+
let tableDepth = 0;
|
|
75
|
+
let row = null;
|
|
76
|
+
let cell = null;
|
|
77
|
+
for (const token of xmlTokens(xml)) {
|
|
78
|
+
const name = token.name;
|
|
79
|
+
if (token.kind === 'text') {
|
|
80
|
+
if (inText)
|
|
81
|
+
paragraph += token.text;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (name === 'w:t')
|
|
85
|
+
inText = token.kind === 'open';
|
|
86
|
+
else if (name === 'w:tab' && token.kind !== 'close')
|
|
87
|
+
paragraph += '\t';
|
|
88
|
+
else if ((name === 'w:br' || name === 'w:cr') && token.kind !== 'close')
|
|
89
|
+
paragraph += '\n';
|
|
90
|
+
else if (name === 'w:pStyle') {
|
|
91
|
+
const level = /^heading\s*(\d)$/i.exec(attrOf(token.attrs, 'w:val') ?? '')?.[1] ?? (/^title$/i.test(attrOf(token.attrs, 'w:val') ?? '') ? '1' : '');
|
|
92
|
+
heading = level ? Number(level) : 0;
|
|
93
|
+
}
|
|
94
|
+
else if (name === 'w:p' && token.kind === 'open') {
|
|
95
|
+
paragraph = '';
|
|
96
|
+
heading = 0;
|
|
97
|
+
}
|
|
98
|
+
else if (name === 'w:p' && token.kind !== 'open') {
|
|
99
|
+
const text = paragraph.replace(/[ ]+$/g, '');
|
|
100
|
+
if (cell)
|
|
101
|
+
cell.push(text.trim());
|
|
102
|
+
else if (text.trim())
|
|
103
|
+
lines.push(heading ? `${'#'.repeat(heading)} ${text.trim()}` : text);
|
|
104
|
+
paragraph = '';
|
|
105
|
+
}
|
|
106
|
+
else if (name === 'w:tbl') {
|
|
107
|
+
tableDepth += token.kind === 'open' ? 1 : token.kind === 'close' ? -1 : 0;
|
|
108
|
+
}
|
|
109
|
+
else if (name === 'w:tr' && tableDepth === 1) {
|
|
110
|
+
if (token.kind === 'open')
|
|
111
|
+
row = [];
|
|
112
|
+
else if (row) {
|
|
113
|
+
if (row.some(one => one))
|
|
114
|
+
lines.push(`| ${row.join(' | ')} |`);
|
|
115
|
+
row = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (name === 'w:tc' && tableDepth === 1) {
|
|
119
|
+
if (token.kind === 'open')
|
|
120
|
+
cell = [];
|
|
121
|
+
else if (cell) {
|
|
122
|
+
row?.push(cell.filter(Boolean).join(' '));
|
|
123
|
+
cell = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return lines.join('\n');
|
|
128
|
+
}
|
|
129
|
+
/* What a slide or its notes repeat on every page - its number, date, footer, header - is not its content. */
|
|
130
|
+
const REPEATED_PLACEHOLDERS = ['sldNum', 'dt', 'ftr', 'hdr'];
|
|
131
|
+
/** The text of one slide or notes part: each paragraph on its own line, the repeated placeholders left out. */
|
|
132
|
+
export function drawingText(xml) {
|
|
133
|
+
const lines = [];
|
|
134
|
+
let paragraph = '';
|
|
135
|
+
let inText = false;
|
|
136
|
+
let repeated = false;
|
|
137
|
+
for (const token of xmlTokens(xml)) {
|
|
138
|
+
if (token.kind === 'text') {
|
|
139
|
+
if (inText)
|
|
140
|
+
paragraph += token.text;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (token.name === 'p:sp' && token.kind === 'open')
|
|
144
|
+
repeated = false;
|
|
145
|
+
else if (token.name === 'p:ph' && token.kind !== 'close')
|
|
146
|
+
repeated = REPEATED_PLACEHOLDERS.includes(attrOf(token.attrs, 'type') ?? '');
|
|
147
|
+
else if (token.name === 'a:t')
|
|
148
|
+
inText = token.kind === 'open';
|
|
149
|
+
else if (token.name === 'a:br' && token.kind !== 'close')
|
|
150
|
+
paragraph += '\n';
|
|
151
|
+
else if (token.name === 'a:p' && token.kind !== 'open') {
|
|
152
|
+
if (paragraph.trim() && !repeated)
|
|
153
|
+
lines.push(paragraph.trim());
|
|
154
|
+
paragraph = '';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return lines;
|
|
158
|
+
}
|
|
159
|
+
/** The slides in the order the presentation shows them - its slide list read through its relationships. */
|
|
160
|
+
export function slideOrder(presentationXml, relsXml) {
|
|
161
|
+
const targets = new Map();
|
|
162
|
+
for (const token of xmlTokens(relsXml)) {
|
|
163
|
+
if (token.name !== 'Relationship')
|
|
164
|
+
continue;
|
|
165
|
+
const id = attrOf(token.attrs, 'Id');
|
|
166
|
+
const target = attrOf(token.attrs, 'Target');
|
|
167
|
+
if (id && target)
|
|
168
|
+
targets.set(id, target.replace(/^\/?ppt\//, '').replace(/^\.\//, ''));
|
|
169
|
+
}
|
|
170
|
+
const order = [];
|
|
171
|
+
for (const token of xmlTokens(presentationXml)) {
|
|
172
|
+
if (token.name !== 'p:sldId')
|
|
173
|
+
continue;
|
|
174
|
+
const target = targets.get(attrOf(token.attrs, 'r:id') ?? '');
|
|
175
|
+
if (target)
|
|
176
|
+
order.push(`ppt/${target}`);
|
|
177
|
+
}
|
|
178
|
+
return order;
|
|
179
|
+
}
|
|
180
|
+
/** The notes part a slide points at, if it has one. */
|
|
181
|
+
export function notesTarget(slidePath, slideRelsXml) {
|
|
182
|
+
for (const token of xmlTokens(slideRelsXml)) {
|
|
183
|
+
if (token.name !== 'Relationship' || !/notesSlide$/.test(attrOf(token.attrs, 'Type') ?? ''))
|
|
184
|
+
continue;
|
|
185
|
+
const target = attrOf(token.attrs, 'Target');
|
|
186
|
+
if (!target)
|
|
187
|
+
continue;
|
|
188
|
+
const base = slidePath.split('/').slice(0, -1);
|
|
189
|
+
for (const part of target.split('/')) {
|
|
190
|
+
if (part === '..')
|
|
191
|
+
base.pop();
|
|
192
|
+
else if (part !== '.')
|
|
193
|
+
base.push(part);
|
|
194
|
+
}
|
|
195
|
+
return base.join('/');
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
/** One slide as text: its paragraphs, then its speaker notes. */
|
|
200
|
+
export function slideText(number, slide, notes) {
|
|
201
|
+
return [`--- slide ${number} ---`, ...slide, ...(notes.length ? ['notes:', ...notes] : [])].join('\n');
|
|
202
|
+
}
|
|
203
|
+
/** One PDF page's text items as lines - pdf.js marks the end of a line on the item that ends it. */
|
|
204
|
+
export function pdfPageText(items) {
|
|
205
|
+
let text = '';
|
|
206
|
+
for (const item of items) {
|
|
207
|
+
text += item.str ?? '';
|
|
208
|
+
if (item.hasEOL)
|
|
209
|
+
text += '\n';
|
|
210
|
+
}
|
|
211
|
+
return text
|
|
212
|
+
.split('\n')
|
|
213
|
+
.map(line => line.replace(/[ \t]+/g, ' ').trim())
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.join('\n');
|
|
216
|
+
}
|
|
217
|
+
/** A page with fewer letters than this is a scan - its text is a page number or a stray mark, not its content. */
|
|
218
|
+
export const SCANNED_PAGE_LETTERS = 16;
|
|
219
|
+
export const isScannedPage = (text) => text.replace(/\s/g, '').length < SCANNED_PAGE_LETTERS;
|
|
220
|
+
/** The header line of a document's text - what it was and how it is marked. */
|
|
221
|
+
export function documentHeader(fileName, kind, count) {
|
|
222
|
+
const many = (word) => `${count} ${word}${count === 1 ? '' : 's'}`;
|
|
223
|
+
if (kind === 'pdf')
|
|
224
|
+
return `# ${fileName} · PDF · ${many('page')} · each page begins with "--- page N ---", N its own page number`;
|
|
225
|
+
if (kind === 'pptx')
|
|
226
|
+
return `# ${fileName} · PowerPoint · ${many('slide')} · each slide begins with "--- slide N ---", its notes after "notes:"`;
|
|
227
|
+
return `# ${fileName} · Word · read in order, headings led by "#", table rows between "|"`;
|
|
228
|
+
}
|
|
229
|
+
/** A finished text checked against the limit and for emptiness - the same three answers for every kind. */
|
|
230
|
+
function settle(fileName, text, body, maxChars, images = []) {
|
|
231
|
+
if (!body.trim() && !images.length)
|
|
232
|
+
return { refusal: { code: 'attachment-document-empty', name: fileName } };
|
|
233
|
+
if (text.length > maxChars)
|
|
234
|
+
return { refusal: { code: 'attachment-document-too-long', name: fileName, maxChars } };
|
|
235
|
+
return { files: { text: { name: documentTextName(fileName), text }, images } };
|
|
236
|
+
}
|
|
237
|
+
export async function readDocx(data, fileName, maxChars, zip) {
|
|
238
|
+
let body;
|
|
239
|
+
try {
|
|
240
|
+
const archive = await zip.loadAsync(data);
|
|
241
|
+
const part = archive.file('word/document.xml');
|
|
242
|
+
if (!part)
|
|
243
|
+
return { refusal: { code: 'attachment-document-unreadable', name: fileName } };
|
|
244
|
+
body = docxText(await part.async('string'));
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
return { refusal: { code: 'attachment-document-unreadable', name: fileName } };
|
|
248
|
+
}
|
|
249
|
+
return settle(fileName, `${documentHeader(fileName, 'docx')}\n${body}\n`, body, maxChars);
|
|
250
|
+
}
|
|
251
|
+
export async function readPptx(data, fileName, maxChars, zip) {
|
|
252
|
+
const slides = [];
|
|
253
|
+
try {
|
|
254
|
+
const archive = await zip.loadAsync(data);
|
|
255
|
+
const read = async (path) => (await archive.file(path)?.async('string')) ?? null;
|
|
256
|
+
const presentation = await read('ppt/presentation.xml');
|
|
257
|
+
const rels = await read('ppt/_rels/presentation.xml.rels');
|
|
258
|
+
if (!presentation || !rels)
|
|
259
|
+
return { refusal: { code: 'attachment-document-unreadable', name: fileName } };
|
|
260
|
+
const order = slideOrder(presentation, rels);
|
|
261
|
+
for (const [index, path] of order.entries()) {
|
|
262
|
+
const slide = await read(path);
|
|
263
|
+
if (slide === null)
|
|
264
|
+
continue;
|
|
265
|
+
const slideRels = await read(path.replace(/slides\/(slide\d+\.xml)$/, 'slides/_rels/$1.rels'));
|
|
266
|
+
const notesPath = slideRels ? notesTarget(path, slideRels) : null;
|
|
267
|
+
const notes = notesPath ? await read(notesPath) : null;
|
|
268
|
+
slides.push(slideText(index + 1, drawingText(slide), notes ? drawingText(notes) : []));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return { refusal: { code: 'attachment-document-unreadable', name: fileName } };
|
|
273
|
+
}
|
|
274
|
+
const body = slides.join('\n');
|
|
275
|
+
const said = slides.some(slide => slide.split('\n').length > 1);
|
|
276
|
+
return settle(fileName, `${documentHeader(fileName, 'pptx', slides.length)}\n${body}\n`, said ? body : '', maxChars);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Every page of a PDF as text, its scanned pages drawn as images by `drawPage` (the screen's canvas). A scan cannot be
|
|
280
|
+
* read unless the mode takes images, and no more scanned pages are drawn than the message may carry - both refused by
|
|
281
|
+
* name, never dropped.
|
|
282
|
+
*/
|
|
283
|
+
export async function readPdf(open, fileName, maxChars, scans, drawPage) {
|
|
284
|
+
const pages = [];
|
|
285
|
+
const scanned = [];
|
|
286
|
+
let count = 0;
|
|
287
|
+
try {
|
|
288
|
+
const pdf = await open();
|
|
289
|
+
count = pdf.numPages;
|
|
290
|
+
for (let number = 1; number <= count; number++) {
|
|
291
|
+
const page = await pdf.getPage(number);
|
|
292
|
+
const text = pdfPageText((await page.getTextContent()).items);
|
|
293
|
+
if (isScannedPage(text)) {
|
|
294
|
+
scanned.push(number);
|
|
295
|
+
pages.push(`--- page ${number} ---\n(no text on this page - a scan, sent as the image "${scannedPageName(fileName, number)}")`);
|
|
296
|
+
}
|
|
297
|
+
else
|
|
298
|
+
pages.push(`--- page ${number} ---\n${text}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return { refusal: { code: 'attachment-document-unreadable', name: fileName } };
|
|
303
|
+
}
|
|
304
|
+
if (scanned.length && !scans.allowed)
|
|
305
|
+
return { refusal: { code: 'attachment-scan-needs-images', name: fileName } };
|
|
306
|
+
if (scanned.length > scans.max)
|
|
307
|
+
return { refusal: { code: 'attachment-scan-too-many-pages', name: fileName, pages: scanned.length, max: scans.max } };
|
|
308
|
+
const images = [];
|
|
309
|
+
try {
|
|
310
|
+
for (const page of scanned)
|
|
311
|
+
images.push({ name: scannedPageName(fileName, page), page, blob: await drawPage(page) });
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return { refusal: { code: 'attachment-document-unreadable', name: fileName } };
|
|
315
|
+
}
|
|
316
|
+
const body = pages.join('\n');
|
|
317
|
+
const read = scanned.length < count ? body : '';
|
|
318
|
+
return settle(fileName, `${documentHeader(fileName, 'pdf', count)}\n${body}\n`, read, maxChars, images);
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=document-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-text.js","sourceRoot":"","sources":["../../client/utils/document-text.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A document read into text the conversation can carry (design/ai/shared-dock.md §9-6) - PDF, Word (.docx) and\n * PowerPoint (.pptx), as a spreadsheet is (spreadsheet-text.ts): the door takes images and text only, so a mode that\n * opens 'document' has the screen read the file before sending (2026-10-28, the user: 「pdf, word, ppt 파일은 처리할\n * 수 없는가?」).\n *\n * - Every page and slide is marked with its own number, so an answer can say \"page 3\" or \"slide 5\" and be checked.\n * - A PDF page with no text on it is a scan: it is drawn into an image and sent beside the text, named by its page.\n * - The old binary .doc and .ppt are refused by name, as .xls is - nothing here opens them.\n * - The limit is on the text after reading. A document over it is refused, never cut: a cut contract is read as the\n * whole contract.\n * - Word keeps no pages (they are laid out when printed), so a .docx is read in order with its headings marked.\n */\n\nexport type DocumentKind = 'pdf' | 'docx' | 'pptx' | 'doc' | 'ppt'\n\nconst PDF_TYPE = 'application/pdf'\nconst DOCX_TYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'\nconst PPTX_TYPE = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'\nconst DOC_TYPE = 'application/msword'\nconst PPT_TYPE = 'application/vnd.ms-powerpoint'\n\n/** The picker's `accept` for documents. */\nexport const DOCUMENT_ACCEPT = ['.pdf', PDF_TYPE, '.docx', DOCX_TYPE, '.pptx', PPTX_TYPE]\n\n/** A document by its type, or by its name when the browser did not know the type. */\nexport function documentKindOf(file: { type?: string; name?: string }): DocumentKind | null {\n const type = (file.type || '').toLowerCase()\n const name = (file.name || '').toLowerCase()\n if (type === PDF_TYPE || name.endsWith('.pdf')) return 'pdf'\n if (type === DOCX_TYPE || name.endsWith('.docx')) return 'docx'\n if (type === PPTX_TYPE || name.endsWith('.pptx')) return 'pptx'\n if (type === DOC_TYPE || name.endsWith('.doc')) return 'doc'\n if (type === PPT_TYPE || name.endsWith('.ppt')) return 'ppt'\n return null\n}\n\nexport type DocumentRefusal =\n | { code: 'attachment-old-office-not-read'; name: string }\n | { code: 'attachment-document-unreadable'; name: string }\n | { code: 'attachment-document-empty'; name: string }\n | { code: 'attachment-document-too-long'; name: string; maxChars: number }\n | { code: 'attachment-scan-needs-images'; name: string }\n | { code: 'attachment-scan-too-many-pages'; name: string; pages: number; max: number }\n\n/** What a document becomes: one text file, and the scanned pages of a PDF as images. */\nexport interface DocumentFiles {\n text: { name: string; text: string }\n images: { name: string; page: number; blob: Blob }[]\n}\n\n/** The file name the text travels under - \"계약서.pdf\" → \"계약서.pdf.txt\", so the source stays readable. */\nexport const documentTextName = (fileName: string) => `${fileName}.txt`\n\n/** The file name a scanned page travels under - \"계약서.pdf\" page 3 → \"계약서.pdf - p3.jpg\". */\nexport const scannedPageName = (fileName: string, page: number) => `${fileName} - p${page}.jpg`\n\n/* ── OOXML, read without a DOM: a small pull reader over the tags the text lives in ─────────────────────────── */\n\nexport interface XmlToken {\n kind: 'open' | 'close' | 'empty' | 'text'\n name?: string\n attrs?: string\n text?: string\n}\n\nconst ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\" }\n\nexport function xmlDecode(text: string): string {\n return text.replace(/&(#x[0-9a-f]+|#\\d+|[a-z]+);/gi, (whole, code: string) => {\n if (code[0] === '#') {\n const n = code[1] === 'x' || code[1] === 'X' ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10)\n return Number.isFinite(n) ? String.fromCodePoint(n) : whole\n }\n return ENTITIES[code.toLowerCase()] ?? whole\n })\n}\n\n/** Tags and text in document order. Comments, declarations and processing instructions are skipped. */\nexport function* xmlTokens(xml: string): Generator<XmlToken> {\n const pattern = /<!--[\\s\\S]*?-->|<\\?[\\s\\S]*?\\?>|<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|<!(?:[^>]*)>|<(\\/?)([\\w:.-]+)([^>]*?)(\\/?)>|([^<]+)/g\n for (const match of xml.matchAll(pattern)) {\n const [, cdata, slash, name, attrs, selfClose, text] = match\n if (cdata !== undefined) yield { kind: 'text', text: cdata }\n else if (name) yield { kind: slash ? 'close' : selfClose ? 'empty' : 'open', name, attrs }\n else if (text !== undefined) yield { kind: 'text', text: xmlDecode(text) }\n }\n}\n\nconst attrOf = (attrs: string | undefined, name: string) => new RegExp(`\\\\b${name}=\"([^\"]*)\"`).exec(attrs ?? '')?.[1]\n\n/**\n * A Word body as text: paragraphs in order, a heading led by \"#\" for its level, a table as one line per row with its\n * cells between \" | \". Text inside a table cell is joined with spaces, so a row stays one line.\n */\nexport function docxText(xml: string): string {\n const lines: string[] = []\n let paragraph = ''\n let heading = 0\n let inText = false\n let tableDepth = 0\n let row: string[] | null = null\n let cell: string[] | null = null\n\n for (const token of xmlTokens(xml)) {\n const name = token.name\n if (token.kind === 'text') {\n if (inText) paragraph += token.text\n continue\n }\n if (name === 'w:t') inText = token.kind === 'open'\n else if (name === 'w:tab' && token.kind !== 'close') paragraph += '\\t'\n else if ((name === 'w:br' || name === 'w:cr') && token.kind !== 'close') paragraph += '\\n'\n else if (name === 'w:pStyle') {\n const level = /^heading\\s*(\\d)$/i.exec(attrOf(token.attrs, 'w:val') ?? '')?.[1] ?? (/^title$/i.test(attrOf(token.attrs, 'w:val') ?? '') ? '1' : '')\n heading = level ? Number(level) : 0\n } else if (name === 'w:p' && token.kind === 'open') {\n paragraph = ''\n heading = 0\n } else if (name === 'w:p' && token.kind !== 'open') {\n const text = paragraph.replace(/[ ]+$/g, '')\n if (cell) cell.push(text.trim())\n else if (text.trim()) lines.push(heading ? `${'#'.repeat(heading)} ${text.trim()}` : text)\n paragraph = ''\n } else if (name === 'w:tbl') {\n tableDepth += token.kind === 'open' ? 1 : token.kind === 'close' ? -1 : 0\n } else if (name === 'w:tr' && tableDepth === 1) {\n if (token.kind === 'open') row = []\n else if (row) {\n if (row.some(one => one)) lines.push(`| ${row.join(' | ')} |`)\n row = null\n }\n } else if (name === 'w:tc' && tableDepth === 1) {\n if (token.kind === 'open') cell = []\n else if (cell) {\n row?.push(cell.filter(Boolean).join(' '))\n cell = null\n }\n }\n }\n return lines.join('\\n')\n}\n\n/* What a slide or its notes repeat on every page - its number, date, footer, header - is not its content. */\nconst REPEATED_PLACEHOLDERS = ['sldNum', 'dt', 'ftr', 'hdr']\n\n/** The text of one slide or notes part: each paragraph on its own line, the repeated placeholders left out. */\nexport function drawingText(xml: string): string[] {\n const lines: string[] = []\n let paragraph = ''\n let inText = false\n let repeated = false\n for (const token of xmlTokens(xml)) {\n if (token.kind === 'text') {\n if (inText) paragraph += token.text\n continue\n }\n if (token.name === 'p:sp' && token.kind === 'open') repeated = false\n else if (token.name === 'p:ph' && token.kind !== 'close') repeated = REPEATED_PLACEHOLDERS.includes(attrOf(token.attrs, 'type') ?? '')\n else if (token.name === 'a:t') inText = token.kind === 'open'\n else if (token.name === 'a:br' && token.kind !== 'close') paragraph += '\\n'\n else if (token.name === 'a:p' && token.kind !== 'open') {\n if (paragraph.trim() && !repeated) lines.push(paragraph.trim())\n paragraph = ''\n }\n }\n return lines\n}\n\n/** The slides in the order the presentation shows them - its slide list read through its relationships. */\nexport function slideOrder(presentationXml: string, relsXml: string): string[] {\n const targets = new Map<string, string>()\n for (const token of xmlTokens(relsXml)) {\n if (token.name !== 'Relationship') continue\n const id = attrOf(token.attrs, 'Id')\n const target = attrOf(token.attrs, 'Target')\n if (id && target) targets.set(id, target.replace(/^\\/?ppt\\//, '').replace(/^\\.\\//, ''))\n }\n const order: string[] = []\n for (const token of xmlTokens(presentationXml)) {\n if (token.name !== 'p:sldId') continue\n const target = targets.get(attrOf(token.attrs, 'r:id') ?? '')\n if (target) order.push(`ppt/${target}`)\n }\n return order\n}\n\n/** The notes part a slide points at, if it has one. */\nexport function notesTarget(slidePath: string, slideRelsXml: string): string | null {\n for (const token of xmlTokens(slideRelsXml)) {\n if (token.name !== 'Relationship' || !/notesSlide$/.test(attrOf(token.attrs, 'Type') ?? '')) continue\n const target = attrOf(token.attrs, 'Target')\n if (!target) continue\n const base = slidePath.split('/').slice(0, -1)\n for (const part of target.split('/')) {\n if (part === '..') base.pop()\n else if (part !== '.') base.push(part)\n }\n return base.join('/')\n }\n return null\n}\n\n/** One slide as text: its paragraphs, then its speaker notes. */\nexport function slideText(number: number, slide: string[], notes: string[]): string {\n return [`--- slide ${number} ---`, ...slide, ...(notes.length ? ['notes:', ...notes] : [])].join('\\n')\n}\n\n/** One PDF page's text items as lines - pdf.js marks the end of a line on the item that ends it. */\nexport function pdfPageText(items: { str?: string; hasEOL?: boolean }[]): string {\n let text = ''\n for (const item of items) {\n text += item.str ?? ''\n if (item.hasEOL) text += '\\n'\n }\n return text\n .split('\\n')\n .map(line => line.replace(/[ \\t]+/g, ' ').trim())\n .filter(Boolean)\n .join('\\n')\n}\n\n/** A page with fewer letters than this is a scan - its text is a page number or a stray mark, not its content. */\nexport const SCANNED_PAGE_LETTERS = 16\n\nexport const isScannedPage = (text: string) => text.replace(/\\s/g, '').length < SCANNED_PAGE_LETTERS\n\n/** The header line of a document's text - what it was and how it is marked. */\nexport function documentHeader(fileName: string, kind: 'pdf' | 'docx' | 'pptx', count?: number): string {\n const many = (word: string) => `${count} ${word}${count === 1 ? '' : 's'}`\n if (kind === 'pdf') return `# ${fileName} · PDF · ${many('page')} · each page begins with \"--- page N ---\", N its own page number`\n if (kind === 'pptx') return `# ${fileName} · PowerPoint · ${many('slide')} · each slide begins with \"--- slide N ---\", its notes after \"notes:\"`\n return `# ${fileName} · Word · read in order, headings led by \"#\", table rows between \"|\"`\n}\n\n/** A finished text checked against the limit and for emptiness - the same three answers for every kind. */\nfunction settle(fileName: string, text: string, body: string, maxChars: number, images: DocumentFiles['images'] = []): { files: DocumentFiles } | { refusal: DocumentRefusal } {\n if (!body.trim() && !images.length) return { refusal: { code: 'attachment-document-empty', name: fileName } }\n if (text.length > maxChars) return { refusal: { code: 'attachment-document-too-long', name: fileName, maxChars } }\n return { files: { text: { name: documentTextName(fileName), text }, images } }\n}\n\n/** What reads a zip - JSZip, passed in so the screen loads it only when a document is attached. */\nexport interface ZipReader {\n loadAsync(data: ArrayBuffer | Uint8Array): Promise<{ file(path: string): { async(type: 'string'): Promise<string> } | null }>\n}\n\nexport async function readDocx(data: ArrayBuffer | Uint8Array, fileName: string, maxChars: number, zip: ZipReader) {\n let body: string\n try {\n const archive = await zip.loadAsync(data)\n const part = archive.file('word/document.xml')\n if (!part) return { refusal: { code: 'attachment-document-unreadable', name: fileName } as DocumentRefusal }\n body = docxText(await part.async('string'))\n } catch {\n return { refusal: { code: 'attachment-document-unreadable', name: fileName } as DocumentRefusal }\n }\n return settle(fileName, `${documentHeader(fileName, 'docx')}\\n${body}\\n`, body, maxChars)\n}\n\nexport async function readPptx(data: ArrayBuffer | Uint8Array, fileName: string, maxChars: number, zip: ZipReader) {\n const slides: string[] = []\n try {\n const archive = await zip.loadAsync(data)\n const read = async (path: string) => (await archive.file(path)?.async('string')) ?? null\n const presentation = await read('ppt/presentation.xml')\n const rels = await read('ppt/_rels/presentation.xml.rels')\n if (!presentation || !rels) return { refusal: { code: 'attachment-document-unreadable', name: fileName } as DocumentRefusal }\n const order = slideOrder(presentation, rels)\n for (const [index, path] of order.entries()) {\n const slide = await read(path)\n if (slide === null) continue\n const slideRels = await read(path.replace(/slides\\/(slide\\d+\\.xml)$/, 'slides/_rels/$1.rels'))\n const notesPath = slideRels ? notesTarget(path, slideRels) : null\n const notes = notesPath ? await read(notesPath) : null\n slides.push(slideText(index + 1, drawingText(slide), notes ? drawingText(notes) : []))\n }\n } catch {\n return { refusal: { code: 'attachment-document-unreadable', name: fileName } as DocumentRefusal }\n }\n const body = slides.join('\\n')\n const said = slides.some(slide => slide.split('\\n').length > 1)\n return settle(fileName, `${documentHeader(fileName, 'pptx', slides.length)}\\n${body}\\n`, said ? body : '', maxChars)\n}\n\n/** What reads a PDF - pdf.js's document, narrowed to what is used, so a test can hand in pages. */\nexport interface PdfSource {\n numPages: number\n getPage(number: number): Promise<{ getTextContent(): Promise<{ items: any[] }> }>\n}\n\n/**\n * Every page of a PDF as text, its scanned pages drawn as images by `drawPage` (the screen's canvas). A scan cannot be\n * read unless the mode takes images, and no more scanned pages are drawn than the message may carry - both refused by\n * name, never dropped.\n */\nexport async function readPdf(\n open: () => Promise<PdfSource>,\n fileName: string,\n maxChars: number,\n scans: { allowed: boolean; max: number },\n drawPage: (number: number) => Promise<Blob>\n): Promise<{ files: DocumentFiles } | { refusal: DocumentRefusal }> {\n const pages: string[] = []\n const scanned: number[] = []\n let count = 0\n try {\n const pdf = await open()\n count = pdf.numPages\n for (let number = 1; number <= count; number++) {\n const page = await pdf.getPage(number)\n const text = pdfPageText((await page.getTextContent()).items)\n if (isScannedPage(text)) {\n scanned.push(number)\n pages.push(`--- page ${number} ---\\n(no text on this page - a scan, sent as the image \"${scannedPageName(fileName, number)}\")`)\n } else pages.push(`--- page ${number} ---\\n${text}`)\n }\n } catch {\n return { refusal: { code: 'attachment-document-unreadable', name: fileName } }\n }\n if (scanned.length && !scans.allowed) return { refusal: { code: 'attachment-scan-needs-images', name: fileName } }\n if (scanned.length > scans.max) return { refusal: { code: 'attachment-scan-too-many-pages', name: fileName, pages: scanned.length, max: scans.max } }\n const images: DocumentFiles['images'] = []\n try {\n for (const page of scanned) images.push({ name: scannedPageName(fileName, page), page, blob: await drawPage(page) })\n } catch {\n return { refusal: { code: 'attachment-document-unreadable', name: fileName } }\n }\n const body = pages.join('\\n')\n const read = scanned.length < count ? body : ''\n return settle(fileName, `${documentHeader(fileName, 'pdf', count)}\\n${body}\\n`, read, maxChars, images)\n}\n"]}
|
|
@@ -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
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-hash.js","sourceRoot":"","sources":["../../client/utils/entry-hash.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,SAAS,GAAG,CAAC,KAAc,EAAU,EAAE;IAC3C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACxD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IACtE,IAAI,OAAO,KAAK,KAAK,QAAQ;QAC3B,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC;aACpC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAE,KAAa,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;aAClD,IAAI,EAAE;aACN,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,SAAS,CAAE,KAAa,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;aACzE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IACjB,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC,CAAA;AAED,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB,CAAC,QAAa;IAC7C,MAAM,IAAI,GAAG,SAAS,CAAC;QACrB,GAAG,EAAE,QAAQ,EAAE,GAAG;QAClB,MAAM,EAAE,QAAQ,EAAE,MAAM;QACxB,EAAE,EAAE,QAAQ,EAAE,EAAE,IAAI,QAAQ;QAC5B,IAAI,EAAE,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;KAChH,CAAC,CAAA;IACF,IAAI,IAAI,GAAG,UAAU,CAAA;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAC1B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAA;AAC5E,CAAC","sourcesContent":["/*\n * One 입력안's fingerprint - what the AI
|
|
1
|
+
{"version":3,"file":"entry-hash.js","sourceRoot":"","sources":["../../client/utils/entry-hash.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,SAAS,GAAG,CAAC,KAAc,EAAU,EAAE;IAC3C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACxD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IACtE,IAAI,OAAO,KAAK,KAAK,QAAQ;QAC3B,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC;aACpC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAE,KAAa,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC;aAClD,IAAI,EAAE;aACN,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,SAAS,CAAE,KAAa,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;aACzE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IACjB,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;AAC9B,CAAC,CAAA;AAED,+EAA+E;AAC/E,MAAM,UAAU,iBAAiB,CAAC,QAAa;IAC7C,MAAM,IAAI,GAAG,SAAS,CAAC;QACrB,GAAG,EAAE,QAAQ,EAAE,GAAG;QAClB,MAAM,EAAE,QAAQ,EAAE,MAAM;QACxB,EAAE,EAAE,QAAQ,EAAE,EAAE,IAAI,QAAQ;QAC5B,IAAI,EAAE,CAAC,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,IAAI,EAAE,EAAE,CAAC,CAAC;KAChH,CAAC,CAAA;IACF,IAAI,IAAI,GAAG,UAAU,CAAA;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QAC1B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAA;AAC5E,CAAC","sourcesContent":["/*\n * One 입력안's fingerprint - what the AI 작업 패널 showed and what the card enters are the same proposal\n * (operato-application/design/ai/workspace.md §6-1). The screen's copy and the server's are one rule;\n * `test/entry-hash-agree.test.ts` runs both over one table.\n *\n * It covers what is entered: the key, the target, create or change, and each row's index, record and values.\n */\n\nconst canonical = (value: unknown): string => {\n if (value === null || value === undefined) return 'null'\n if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`\n if (typeof value === 'object')\n return `{${Object.keys(value as object)\n .filter(name => (value as any)[name] !== undefined)\n .sort()\n .map(name => `${JSON.stringify(name)}:${canonical((value as any)[name])}`)\n .join(',')}}`\n return JSON.stringify(value)\n}\n\n/** FNV-1a over the canonical text - a fingerprint to compare, not a secret. */\nexport function entryProposalHash(proposal: any): string {\n const text = canonical({\n key: proposal?.key,\n target: proposal?.target,\n op: proposal?.op ?? 'create',\n rows: (proposal?.rows ?? []).map((row: any) => ({ index: row?.index, id: row?.id, values: row?.values ?? {} }))\n })\n let hash = 0x811c9dc5\n for (let i = 0; i < text.length; i++) {\n hash ^= text.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193) >>> 0\n }\n return `${hash.toString(16).padStart(8, '0')}-${text.length.toString(16)}`\n}\n"]}
|