dsh-doc-assistant 0.1.14 → 0.1.19
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/README.md +32 -2
- package/lib/client.js +615 -196
- package/package.json +4 -3
- package/src/client/20-api.js +2 -0
- package/src/client/40-revisions.js +67 -24
- package/src/client/50-library.js +1 -1
- package/src/client/60-reader.js +522 -147
- package/src/client/70-shelf.js +22 -23
- package/src/host/chat.js +43 -13
- package/src/host/covers.js +56 -15
- package/src/host/docir.js +26 -3
- package/src/host/legacyWord.js +91 -0
- package/src/host/modelVision.js +44 -0
- package/src/host/ocr.js +120 -210
- package/src/host/ocr_vision.js +39 -0
- package/src/host/ocr_windows.ps1 +89 -0
- package/src/host/office.js +3 -1
- package/src/host/officeImages.js +269 -0
- package/src/host/office_tool.py +451 -29
- package/src/host/preview.js +132 -28
- package/src/host/revise.js +146 -21
- package/src/host/revisionBoundary.js +45 -0
- package/src/host/revisionRebase.js +123 -0
- package/src/host/revisions.js +20 -6
- package/src/host/routes.js +18 -5
- package/src/host/tree.js +1 -0
- package/src/host/upload.js +113 -11
- package/src/host/visionOcr.js +101 -0
- package/src/host/visualContext.js +248 -0
- package/src/host/wordLayout.js +131 -0
- package/src/host/wordSelection.js +114 -0
- package/src/host/writeback.js +108 -19
- package/src/host/writers/index.js +1 -0
package/src/client/60-reader.js
CHANGED
|
@@ -109,7 +109,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
109
109
|
for (const item of items) {
|
|
110
110
|
const expand = item.action === 'expand' || /^扩写/.test(item.instruction || '');
|
|
111
111
|
const key = expand ? JSON.stringify([item.docHash, item.original, item.anchor?.kind,
|
|
112
|
-
item.anchor?.blockId, item.anchor?.srcStart, item.anchor?.srcEnd, item.anchor?.sheet, item.anchor?.cell
|
|
112
|
+
item.anchor?.blockId, item.anchor?.srcStart, item.anchor?.srcEnd, item.anchor?.sheet, item.anchor?.cell,
|
|
113
|
+
item.anchor?.segments?.map(segment => [segment.blockId, segment.srcStart, segment.srcEnd])]) : item.id;
|
|
113
114
|
let group = byKey.get(key);
|
|
114
115
|
if (!group) { group = { key, items: [] }; byKey.set(key, group); groups.push(group); }
|
|
115
116
|
group.items.push(item);
|
|
@@ -120,14 +121,19 @@ __define('./reader', (module, exports, require) => {
|
|
|
120
121
|
|
|
121
122
|
function comparisonChoices(group, currentHash) {
|
|
122
123
|
if (group.items.some(item => item.status === 'accepted')) return [];
|
|
123
|
-
return group.items.filter(item => item.status === 'pending' && !item.deletedAt
|
|
124
|
-
&& (!currentHash || !item.docHash || item.docHash === currentHash));
|
|
124
|
+
return group.items.filter(item => item.status === 'pending' && !item.deletedAt);
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
function captureRequest(value) {
|
|
128
128
|
const request = JSON.parse(JSON.stringify(value));
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const freeze = item => {
|
|
130
|
+
if (item && typeof item === 'object') {
|
|
131
|
+
Object.values(item).forEach(freeze);
|
|
132
|
+
Object.freeze(item);
|
|
133
|
+
}
|
|
134
|
+
return item;
|
|
135
|
+
};
|
|
136
|
+
return freeze(request);
|
|
131
137
|
}
|
|
132
138
|
|
|
133
139
|
// =========================================================================
|
|
@@ -707,7 +713,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
707
713
|
}
|
|
708
714
|
|
|
709
715
|
const LAYOUT_FORMATS = { docx: true, pptx: true, xlsx: true, pdf: true };
|
|
710
|
-
const LAYOUT_ONLY_FORMATS = { docx: true, pptx: true, pdf: true };
|
|
716
|
+
const LAYOUT_ONLY_FORMATS = { doc: true, docx: true, pptx: true, pdf: true };
|
|
711
717
|
|
|
712
718
|
function mediaUrl(route, projectId, path, extra) {
|
|
713
719
|
const q = new URLSearchParams();
|
|
@@ -826,13 +832,39 @@ __define('./reader', (module, exports, require) => {
|
|
|
826
832
|
return { picked, matched, srcStart: matched[0], srcEnd: matched[matched.length - 1] + 1 };
|
|
827
833
|
}
|
|
828
834
|
|
|
835
|
+
function wordLineGeometry(line) {
|
|
836
|
+
const box = line && line.layoutBox;
|
|
837
|
+
return box && box.w > 0 ? { x: (line.x - box.x) / box.w, w: line.w / box.w }
|
|
838
|
+
: { x: Number(line && line.x), w: Number(line && line.w) };
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/** Column membership supplies layout context only; source matching stays exact. */
|
|
842
|
+
function sameWordColumn(a, b) {
|
|
843
|
+
const first = wordLineGeometry(a), next = wordLineGeometry(b);
|
|
844
|
+
if (![first.x, first.w, next.x, next.w].every(Number.isFinite) || first.w <= 0 || next.w <= 0) return false;
|
|
845
|
+
const overlap = Math.min(first.x + first.w, next.x + next.w) - Math.max(first.x, next.x);
|
|
846
|
+
return Math.abs(first.x - next.x) <= .065 && overlap > Math.min(first.w, next.w) * .45;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function exactWordGlyphs(line) {
|
|
850
|
+
if (!line || line.ocr || line.precise !== true || !Array.isArray(line.glyphs) || !line.glyphs.length) return false;
|
|
851
|
+
let cursor = 0;
|
|
852
|
+
for (const glyph of line.glyphs) {
|
|
853
|
+
if (glyph.precise === false || !Number.isInteger(glyph.from) || !Number.isInteger(glyph.to)
|
|
854
|
+
|| glyph.from !== cursor || glyph.to <= cursor || !Number.isFinite(glyph.x) || !Number.isFinite(glyph.w) || glyph.w < 0) return false;
|
|
855
|
+
cursor = glyph.to;
|
|
856
|
+
}
|
|
857
|
+
return cursor === String(line.text || '').length;
|
|
858
|
+
}
|
|
859
|
+
|
|
829
860
|
function bindWordLayoutLines(blocks, lines) {
|
|
830
861
|
const parts = sourceParts({ blocks });
|
|
831
862
|
const source = parts.map((part) => part.text).join('\n');
|
|
832
863
|
const index = wordTextIndex(source);
|
|
833
864
|
const beginnings = parts.map(part => wordTextIndex(part.text).compact);
|
|
834
865
|
const list = Array.isArray(lines) ? lines : [];
|
|
835
|
-
const entries = list.map((
|
|
866
|
+
const entries = list.map((rawLine, order) => {
|
|
867
|
+
const { srcStart, srcEnd, srcMap, blockId, wordCell, wordParagraphs, wordPrefix, wordLayout, ...line } = rawLine || {};
|
|
836
868
|
const text = String(line && line.text || '');
|
|
837
869
|
let prefix = 0;
|
|
838
870
|
let picked = wordTextIndex(text);
|
|
@@ -877,16 +909,56 @@ __define('./reader', (module, exports, require) => {
|
|
|
877
909
|
window.forEach((entry, i) => { entry.at = placements[i]; });
|
|
878
910
|
}
|
|
879
911
|
}
|
|
912
|
+
// Multi-column reports interleave author details with body lines in PDF
|
|
913
|
+
// reading order. Match a unique, exact continuation in its own column,
|
|
914
|
+
// and require any already mapped neighbour to agree with the placement.
|
|
915
|
+
for (const entry of ordered) {
|
|
916
|
+
if (entry.at >= 0 || !exactWordGlyphs(entry.line)) continue;
|
|
917
|
+
const column = ordered.filter(other => exactWordGlyphs(other.line)
|
|
918
|
+
&& Number(other.line.page || 1) === Number(entry.line.page || 1) && sameWordColumn(entry.line, other.line));
|
|
919
|
+
const center = column.indexOf(entry), placements = new Set();
|
|
920
|
+
for (let size = 2; size <= Math.min(8, column.length); size += 1) {
|
|
921
|
+
for (let from = Math.max(0, center - size + 1); from <= center && from + size <= column.length; from += 1) {
|
|
922
|
+
const window = column.slice(from, from + size);
|
|
923
|
+
if (!window.some(other => other !== entry && other.at >= 0)) continue;
|
|
924
|
+
const needle = window.map(other => other.picked.compact).join('');
|
|
925
|
+
if (needle.length > 2048) continue;
|
|
926
|
+
let offset = 0;
|
|
927
|
+
const anchored = new Set();
|
|
928
|
+
for (const other of window) {
|
|
929
|
+
if (other.at >= 0) anchored.add(other.at - offset);
|
|
930
|
+
offset += other.picked.compact.length;
|
|
931
|
+
}
|
|
932
|
+
if (anchored.size !== 1) continue;
|
|
933
|
+
const at = anchored.values().next().value;
|
|
934
|
+
if (at < 0 || index.compact.slice(at, at + needle.length) !== needle) continue;
|
|
935
|
+
let cursor = at, wanted = -1, consistent = true;
|
|
936
|
+
for (const other of window) {
|
|
937
|
+
if (other.at >= 0 && other.at !== cursor) consistent = false;
|
|
938
|
+
if (other === entry) wanted = cursor;
|
|
939
|
+
cursor += other.picked.compact.length;
|
|
940
|
+
}
|
|
941
|
+
if (consistent && !ordered.some(other => other !== entry && other.at >= 0
|
|
942
|
+
&& other.at < wanted + entry.picked.compact.length && other.at + other.picked.compact.length > wanted)) placements.add(wanted);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
if (placements.size === 1) entry.at = placements.values().next().value;
|
|
946
|
+
}
|
|
880
947
|
}
|
|
881
948
|
return entries.map(entry => {
|
|
882
949
|
const { line, text, picked, prefix, at } = entry;
|
|
883
|
-
if (at < 0) return { ...line, text };
|
|
950
|
+
if (at < 0) return { ...line, text, wordLayout: true };
|
|
884
951
|
const matched = index.positions.slice(at, at + picked.compact.length);
|
|
885
952
|
const srcStart = matched[0];
|
|
886
953
|
const srcEnd = matched[matched.length - 1] + 1;
|
|
887
954
|
const part = parts.find(item => srcStart >= item.start && srcStart < item.start + item.text.length);
|
|
888
|
-
|
|
889
|
-
|
|
955
|
+
// PDF-to-Word documents can put a bullet and its text in two separate
|
|
956
|
+
// paragraphs while painting them as one PDF line. Native glyphs prove
|
|
957
|
+
// that exact combined span; OCR and table-cell crossings stay refused.
|
|
958
|
+
const crossed = parts.filter(item => item.start < srcEnd && item.start + item.text.length > srcStart);
|
|
959
|
+
if (!part || (srcEnd > part.start + part.text.length
|
|
960
|
+
&& (!exactWordGlyphs(line) || crossed.some(item => item.block.type === 'table')))) return { ...line, text, wordLayout: true };
|
|
961
|
+
const bound = { ...line, text, wordLayout: true, blockId: part.block.id, srcStart, srcEnd };
|
|
890
962
|
if (part.block.type === 'table') {
|
|
891
963
|
for (let row = 0; row < (part.block.rows || []).length; row++) {
|
|
892
964
|
const cells = part.block.rows[row].cells || [];
|
|
@@ -985,6 +1057,45 @@ __define('./reader', (module, exports, require) => {
|
|
|
985
1057
|
return ranges;
|
|
986
1058
|
}
|
|
987
1059
|
|
|
1060
|
+
function wordListMarker(text) {
|
|
1061
|
+
return /^[\s⚫•●▪◦‣⁃∙○◉◎■□◆◇◼◾⬤*\-]+$/u.test(String(text || ''))
|
|
1062
|
+
&& /[⚫•●▪◦‣⁃∙○◉◎■□◆◇◼◾⬤*\-]/u.test(String(text || ''));
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/** Separate Word paragraphs remain exact source segments of one selection. */
|
|
1066
|
+
function wordCompoundAnchor(doc, parts) {
|
|
1067
|
+
const source = sourceParts(doc).map(part => part.text).join('\n');
|
|
1068
|
+
if (!Array.isArray(parts) || !parts.length || parts.some(part => !part?.anchor
|
|
1069
|
+
|| !Number.isInteger(part.anchor.srcStart) || !Number.isInteger(part.anchor.srcEnd)
|
|
1070
|
+
|| part.anchor.srcStart < 0 || part.anchor.srcEnd <= part.anchor.srcStart || part.anchor.srcEnd > source.length
|
|
1071
|
+
|| source.slice(part.anchor.srcStart, part.anchor.srcEnd) !== part.text)) return null;
|
|
1072
|
+
if (parts.length === 1) return { ...parts[0].anchor };
|
|
1073
|
+
const segments = parts.map(part => ({ blockId: part.anchor.blockId, srcStart: part.anchor.srcStart,
|
|
1074
|
+
srcEnd: part.anchor.srcEnd, selectionProof: part.anchor.selectionProof || 'pdf-glyphs-v1' }));
|
|
1075
|
+
if (segments.some((segment, i) => i > 0 && segment.srcStart < segments[i - 1].srcEnd)) return null;
|
|
1076
|
+
const anchor = { kind: 'range', blockId: segments[0].blockId, segments };
|
|
1077
|
+
const from = segments[0].srcStart, to = segments.at(-1).srcEnd;
|
|
1078
|
+
if (segments.every((segment, i) => !i || /^\s*$/.test(source.slice(segments[i - 1].srcEnd, segment.srcStart)))) {
|
|
1079
|
+
anchor.srcStart = from; anchor.srcEnd = to;
|
|
1080
|
+
}
|
|
1081
|
+
return anchor;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function wordAnchorText(doc, anchor) {
|
|
1085
|
+
const source = sourceParts(doc).map(part => part.text).join('\n');
|
|
1086
|
+
const segments = Array.isArray(anchor?.segments) ? anchor.segments : [anchor];
|
|
1087
|
+
if (!segments.length || segments.some(segment => !Number.isInteger(segment?.srcStart) || !Number.isInteger(segment?.srcEnd)
|
|
1088
|
+
|| segment.srcStart < 0 || segment.srcEnd <= segment.srcStart || segment.srcEnd > source.length)) return undefined;
|
|
1089
|
+
return segments.map(segment => source.slice(segment.srcStart, segment.srcEnd)).join('\n');
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function continuousWordAnchor(doc, anchor) {
|
|
1093
|
+
const source = sourceParts(doc).map(part => part.text).join('\n');
|
|
1094
|
+
const segments = Array.isArray(anchor?.segments) ? anchor.segments : [anchor];
|
|
1095
|
+
return wordAnchorText(doc, anchor) !== undefined && segments.every((segment, i) => !i
|
|
1096
|
+
|| (segment.srcStart >= segments[i - 1].srcEnd && /^\s*$/.test(source.slice(segments[i - 1].srcEnd, segment.srcStart))));
|
|
1097
|
+
}
|
|
1098
|
+
|
|
988
1099
|
/** The physical highlight, quote and rewrite all use this same exact source. */
|
|
989
1100
|
function wordNativeSelection(doc, lines) {
|
|
990
1101
|
const selected = sortReadingOrder(lines);
|
|
@@ -998,8 +1109,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
998
1109
|
if (!original.trim() || wordTextIndex(original).compact !== wordTextIndex(picked).compact) return fallback;
|
|
999
1110
|
const parts = wordRanges(doc, from, to);
|
|
1000
1111
|
if (!parts.length) return fallback;
|
|
1001
|
-
|
|
1002
|
-
|
|
1112
|
+
const anchor = wordCompoundAnchor(doc, parts);
|
|
1113
|
+
if (!anchor) return fallback;
|
|
1114
|
+
return { anchor, text: wordAnchorText(doc, anchor), blockId: parts[0].blockId, parts, verified: true };
|
|
1003
1115
|
}
|
|
1004
1116
|
|
|
1005
1117
|
/** A rectangle can select separate source slices; verify each before merging only whitespace gaps. */
|
|
@@ -1020,8 +1132,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
1020
1132
|
}
|
|
1021
1133
|
if (!groups.length) return null;
|
|
1022
1134
|
const parts = groups.flatMap(group => wordRanges(doc, group.from, group.to));
|
|
1023
|
-
|
|
1024
|
-
|
|
1135
|
+
const anchor = wordCompoundAnchor(doc, parts);
|
|
1136
|
+
if (!anchor) return { anchor: { kind: 'range' }, text: joinOcrText(lines), wordUnmapped: true };
|
|
1137
|
+
return { anchor, text: wordAnchorText(doc, anchor), parts, blockId: parts[0].blockId, verified: true };
|
|
1025
1138
|
}
|
|
1026
1139
|
|
|
1027
1140
|
function uniqueAt(haystack, needle) {
|
|
@@ -1491,12 +1604,26 @@ __define('./reader', (module, exports, require) => {
|
|
|
1491
1604
|
* @param {{ x: number, y: number }} b
|
|
1492
1605
|
*/
|
|
1493
1606
|
function selectionSpans(lines, a, b) {
|
|
1494
|
-
|
|
1607
|
+
let list = sortReadingOrder((Array.isArray(lines) ? lines : []).filter((line) => (
|
|
1495
1608
|
line && (line.ocr === true || line.nativeSelection) && String(line.text || '').trim()
|
|
1496
1609
|
)));
|
|
1497
1610
|
if (!a || !b || list.length === 0) return [];
|
|
1498
|
-
|
|
1499
|
-
|
|
1611
|
+
let ia = indexAtPoint(list, Number(a.x) || 0, Number(a.y) || 0);
|
|
1612
|
+
let ib = indexAtPoint(list, Number(b.x) || 0, Number(b.y) || 0);
|
|
1613
|
+
const first = list[ia >= 0 ? ia : ib], last = list[ib >= 0 ? ib : ia];
|
|
1614
|
+
if (first && first.wordLayout && !first.wordCell && (!last || sameWordColumn(first, last))) {
|
|
1615
|
+
const edge = wordLineGeometry(first);
|
|
1616
|
+
// Restrict an ordinary drag inside a clearly separate Word column. A
|
|
1617
|
+
// rectangle remains available for an intentional multi-column selection.
|
|
1618
|
+
const hasOtherColumn = list.some(line => line.wordLayout && Number(line.page || 1) === Number(first.page || 1)
|
|
1619
|
+
&& Math.abs(wordLineGeometry(line).x - edge.x) > .18
|
|
1620
|
+
&& (wordLineGeometry(line).x >= edge.x + edge.w || wordLineGeometry(line).x + wordLineGeometry(line).w <= edge.x));
|
|
1621
|
+
if (hasOtherColumn) {
|
|
1622
|
+
list = list.filter(line => sameWordColumn(first, line));
|
|
1623
|
+
ia = indexAtPoint(list, Number(a.x) || 0, Number(a.y) || 0);
|
|
1624
|
+
ib = indexAtPoint(list, Number(b.x) || 0, Number(b.y) || 0);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1500
1627
|
if (ia < 0 && ib < 0) {
|
|
1501
1628
|
// 指针停在行外时,仍按拖过的矩形把跨到的行切开,避免一松手选区被清掉。
|
|
1502
1629
|
const hits = hitsInRect(list, { x0: a.x, y0: a.y, x1: b.x, y1: b.y });
|
|
@@ -1995,7 +2122,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
1995
2122
|
function revisionAllowed(doc) {
|
|
1996
2123
|
if (!doc || typeof doc !== 'object') return true;
|
|
1997
2124
|
const format = typeof doc.format === 'string' ? doc.format : '';
|
|
1998
|
-
if (format === 'pdf' || format === 'image') return false;
|
|
2125
|
+
if (format === 'pdf' || format === 'image' || format === 'doc') return false;
|
|
1999
2126
|
return !(doc.meta && doc.meta.writable === false);
|
|
2000
2127
|
}
|
|
2001
2128
|
|
|
@@ -2057,6 +2184,18 @@ __define('./reader', (module, exports, require) => {
|
|
|
2057
2184
|
return `dshDoc:scroll:${projectId}:${path}`;
|
|
2058
2185
|
}
|
|
2059
2186
|
|
|
2187
|
+
function slideStorageKey(projectId, path) {
|
|
2188
|
+
return `dshDoc:slide:${projectId}:${path}`;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function storedSlide(projectId, path) {
|
|
2192
|
+
try {
|
|
2193
|
+
const value = Number(currentWindow()?.localStorage?.getItem(slideStorageKey(projectId, path)));
|
|
2194
|
+
if (Number.isSafeInteger(value) && value >= 1) return value;
|
|
2195
|
+
} catch { /* A private browser still opens the first page. */ }
|
|
2196
|
+
return 1;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2060
2199
|
function isTypingTarget(target) {
|
|
2061
2200
|
if (!target || typeof target.tagName !== 'string') return false;
|
|
2062
2201
|
const tag = target.tagName;
|
|
@@ -2073,7 +2212,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
2073
2212
|
const [findOpen, setFindOpen] = React.useState(false);
|
|
2074
2213
|
const [findQ, setFindQ] = React.useState('');
|
|
2075
2214
|
const [findAt, setFindAt] = React.useState(0);
|
|
2076
|
-
const [slide, setSlide] = React.useState(
|
|
2215
|
+
const [slide, setSlide] = React.useState(() => storedSlide(p.projectId, p.path));
|
|
2216
|
+
const [slideInput, setSlideInput] = React.useState(() => String(storedSlide(p.projectId, p.path)));
|
|
2217
|
+
const [slideJumpError, setSlideJumpError] = React.useState('');
|
|
2077
2218
|
const [slideCount, setSlideCount] = React.useState(0);
|
|
2078
2219
|
const [slideReady, setSlideReady] = React.useState(false);
|
|
2079
2220
|
const [slideError, setSlideError] = React.useState('');
|
|
@@ -2082,6 +2223,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
2082
2223
|
const [pageFailed, setPageFailed] = React.useState(false);
|
|
2083
2224
|
const [readyPages, setReadyPages] = React.useState({});
|
|
2084
2225
|
const [ocrByPage, setOcrByPage] = React.useState({});
|
|
2226
|
+
const [pageTextNotice, setPageTextNotice] = React.useState(null);
|
|
2085
2227
|
const [rasterPages, setRasterPages] = React.useState(0);
|
|
2086
2228
|
const [rasterAspect, setRasterAspect] = React.useState(0);
|
|
2087
2229
|
const [ocrOn, setOcrOn] = React.useState(null);
|
|
@@ -2093,6 +2235,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
2093
2235
|
const bodyRef = React.useRef(null);
|
|
2094
2236
|
const wrapRef = React.useRef(null);
|
|
2095
2237
|
const slideCountRef = React.useRef(0);
|
|
2238
|
+
const slideStorageRef = React.useRef({ target: '', ready: false });
|
|
2096
2239
|
const missTick = React.useRef(0);
|
|
2097
2240
|
const gestureRef = React.useRef(null);
|
|
2098
2241
|
const selectionScrollRef = React.useRef(null);
|
|
@@ -2111,7 +2254,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
2111
2254
|
const actions = React.useMemo(() => (
|
|
2112
2255
|
canRevise ? baseActions : baseActions.filter((action) => !action || action.kind !== 'revise')
|
|
2113
2256
|
), [baseActions, canRevise]);
|
|
2114
|
-
const
|
|
2257
|
+
const sourceFormat = doc && typeof doc.format === 'string' ? doc.format : '';
|
|
2258
|
+
const format = sourceFormat === 'doc' ? 'docx' : sourceFormat;
|
|
2259
|
+
const visionModelKey = JSON.stringify(p.visionRoute || {});
|
|
2115
2260
|
const canLayout = Boolean(p.projectId && p.path && LAYOUT_FORMATS[format]);
|
|
2116
2261
|
// Word / PPT / PDF 只显示版式,旧的文本偏好也不能把它们带回抽取正文。
|
|
2117
2262
|
const view = (LAYOUT_ONLY_FORMATS[format] && canLayout) || (p.view === 'layout' && canLayout) ? 'layout' : 'text';
|
|
@@ -2124,9 +2269,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
2124
2269
|
kind: 'pick',
|
|
2125
2270
|
title: '列出这一页的文本框,选一段润色、扩写,或自己改字',
|
|
2126
2271
|
}])
|
|
2127
|
-
: actions)
|
|
2128
|
-
|
|
2129
|
-
), [actions, format, canRevise, showLayout, p.busyAction]);
|
|
2272
|
+
: actions)
|
|
2273
|
+
), [actions, format, canRevise, showLayout]);
|
|
2130
2274
|
const showImage = format === 'image' && !showLayout;
|
|
2131
2275
|
const reason = doc && doc.meta && typeof doc.meta.reason === 'string' ? doc.meta.reason : '';
|
|
2132
2276
|
const previewRev = Number(p.previewEpoch) || 0;
|
|
@@ -2149,6 +2293,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
2149
2293
|
}
|
|
2150
2294
|
function docMedia(route, extra) {
|
|
2151
2295
|
const query = extra ? { ...extra } : {};
|
|
2296
|
+
if (route === 'page-text' && query.page && p.visionRoute?.provider && p.visionRoute?.model) {
|
|
2297
|
+
query.provider = p.visionRoute.provider; query.model = p.visionRoute.model;
|
|
2298
|
+
}
|
|
2152
2299
|
// 旧地址没有 v,浏览器会把改写前的页图缓存五分钟。每次接受都换一个 v。
|
|
2153
2300
|
query.v = previewRev;
|
|
2154
2301
|
return mediaUrl(route, p.projectId, p.path, query);
|
|
@@ -2161,10 +2308,15 @@ __define('./reader', (module, exports, require) => {
|
|
|
2161
2308
|
setFindOpen(false);
|
|
2162
2309
|
setFindQ('');
|
|
2163
2310
|
setFindAt(0);
|
|
2164
|
-
|
|
2311
|
+
const restoredSlide = storedSlide(p.projectId, p.path);
|
|
2312
|
+
slideStorageRef.current = { target: `${p.projectId}\u0000${p.path}`, ready: false };
|
|
2313
|
+
setSlide(restoredSlide);
|
|
2314
|
+
setSlideInput(String(restoredSlide));
|
|
2315
|
+
setSlideJumpError('');
|
|
2165
2316
|
setSlideCount(0);
|
|
2166
2317
|
setSlideReady(false);
|
|
2167
2318
|
setSlideError('');
|
|
2319
|
+
setPageTextNotice(null);
|
|
2168
2320
|
setSlideRatio(16 / 9);
|
|
2169
2321
|
setPageModel(null);
|
|
2170
2322
|
setPageFailed(false);
|
|
@@ -2216,23 +2368,30 @@ __define('./reader', (module, exports, require) => {
|
|
|
2216
2368
|
fetch(docMedia('pages'), { signal: ac.signal })
|
|
2217
2369
|
.then((res) => res.json())
|
|
2218
2370
|
.then((body) => {
|
|
2371
|
+
if (ac.signal.aborted) return;
|
|
2219
2372
|
if (!body || body.ok === false) {
|
|
2220
2373
|
setSlideError((body && body.error) || '无法读取幻灯片页数');
|
|
2221
2374
|
return;
|
|
2222
2375
|
}
|
|
2223
2376
|
const pages = Number(body.pages);
|
|
2377
|
+
const count = Number.isInteger(pages) && pages > 0 ? pages : 1;
|
|
2224
2378
|
setSlideError('');
|
|
2225
|
-
|
|
2379
|
+
slideStorageRef.current = { target: `${p.projectId}\u0000${p.path}`, ready: true };
|
|
2380
|
+
setSlideCount(count);
|
|
2381
|
+
setSlide(value => Math.min(count, Math.max(1, value)));
|
|
2226
2382
|
})
|
|
2227
2383
|
.catch((error) => {
|
|
2228
|
-
if (error && error.name === 'AbortError') return;
|
|
2384
|
+
if (ac.signal.aborted || error && error.name === 'AbortError') return;
|
|
2229
2385
|
setSlideError('无法读取幻灯片页数');
|
|
2230
2386
|
});
|
|
2231
2387
|
return () => ac.abort();
|
|
2232
|
-
}, [format, showLayout, p.projectId, p.path]);
|
|
2388
|
+
}, [format, showLayout, p.projectId, p.path, previewRev]);
|
|
2233
2389
|
|
|
2234
2390
|
React.useEffect(() => {
|
|
2391
|
+
setSlideInput(String(slide));
|
|
2392
|
+
setSlideJumpError('');
|
|
2235
2393
|
setSlideReady(false);
|
|
2394
|
+
setSlideError('');
|
|
2236
2395
|
setSelection(null);
|
|
2237
2396
|
setOcrOn(null);
|
|
2238
2397
|
setBoxPick(null);
|
|
@@ -2241,8 +2400,16 @@ __define('./reader', (module, exports, require) => {
|
|
|
2241
2400
|
}, [slide, p.projectId, p.path]);
|
|
2242
2401
|
|
|
2243
2402
|
React.useEffect(() => {
|
|
2244
|
-
|
|
2245
|
-
|
|
2403
|
+
if (format !== 'pptx' || !slideCount || slide < 1 || slide > slideCount
|
|
2404
|
+
|| !slideStorageRef.current.ready || slideStorageRef.current.target !== `${p.projectId}\u0000${p.path}`) return;
|
|
2405
|
+
try { currentWindow()?.localStorage?.setItem(slideStorageKey(p.projectId, p.path), String(slide)); }
|
|
2406
|
+
catch { /* Navigation does not depend on local persistence. */ }
|
|
2407
|
+
if (typeof p.onLayoutPage === 'function') p.onLayoutPage(slide);
|
|
2408
|
+
}, [format, p.projectId, p.path, slide, slideCount]);
|
|
2409
|
+
|
|
2410
|
+
React.useEffect(() => {
|
|
2411
|
+
// 幻灯片逐页读取;打开图片型 PPTX 不预先转换整册。
|
|
2412
|
+
const want = Boolean(format !== 'pptx' && p.projectId && p.path && (showLayout || format === 'pdf'));
|
|
2246
2413
|
if (!want) return undefined;
|
|
2247
2414
|
const ac = new AbortController();
|
|
2248
2415
|
fetch(docMedia('page-text'), { signal: ac.signal })
|
|
@@ -2291,7 +2458,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
2291
2458
|
if (!nativePages.has(i)) queue.push(i);
|
|
2292
2459
|
}
|
|
2293
2460
|
}
|
|
2294
|
-
const tokens = queue.map((page) => `${p.projectId}:${p.path}:${page}:${previewRev}`);
|
|
2461
|
+
const tokens = queue.map((page) => `${p.projectId}:${p.path}:${page}:${previewRev}:${visionModelKey}`);
|
|
2295
2462
|
(async () => {
|
|
2296
2463
|
for (let i = 0; i < queue.length; i += 1) {
|
|
2297
2464
|
if (cancelled) return;
|
|
@@ -2299,23 +2466,32 @@ __define('./reader', (module, exports, require) => {
|
|
|
2299
2466
|
const token = tokens[i];
|
|
2300
2467
|
if (ocrTried.current[token]) continue;
|
|
2301
2468
|
ocrTried.current[token] = 'pending';
|
|
2469
|
+
if (format === 'pptx') setPageTextNotice({ page, pending: true, text: '正在识别本页文字,完成后即可选中…' });
|
|
2302
2470
|
try {
|
|
2303
2471
|
const res = await fetch(docMedia('page-text', { page }), { signal: ac.signal });
|
|
2304
2472
|
const body = await res.json();
|
|
2305
2473
|
if (cancelled) return;
|
|
2306
2474
|
ocrTried.current[token] = 'done';
|
|
2307
|
-
if (!body || body.ok === false)
|
|
2475
|
+
if (!body || body.ok === false) {
|
|
2476
|
+
if (format === 'pptx') setPageTextNotice({ page, text: body?.error || '本页文字读取失败,请刷新后重试。' });
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
if (format === 'pptx') setPageTextNotice(body.lines?.length
|
|
2480
|
+
? (body.ocr?.warning ? { page, text: body.ocr.warning + (body.ocr.visionError ? ` ${body.ocr.visionError}` : '') } : null)
|
|
2481
|
+
: { page, text: body.ocr?.visionError || body.ocr?.error || '本页未识别到文字;可选用支持图片的模型进行文档提问。' });
|
|
2308
2482
|
const pages = Number(body.pages);
|
|
2309
2483
|
const aspect = Number(body.aspect);
|
|
2310
2484
|
// 第一页可能全是图片;页数仍用于继续读取后面的文字页。
|
|
2311
2485
|
if (pages > 0) setRasterPages((prev) => (pages > prev ? pages : prev));
|
|
2312
2486
|
if (aspect > 0) setRasterAspect((prev) => (prev > 0 ? prev : aspect));
|
|
2313
2487
|
if (!Array.isArray(body.lines) || body.lines.length === 0) continue;
|
|
2314
|
-
if (
|
|
2488
|
+
if (body.source === 'pdf' || body.source === 'mixed') {
|
|
2315
2489
|
setPageModel((prev) => ({
|
|
2316
2490
|
pages: Math.max(pages || 1, prev && prev.pages || 1),
|
|
2317
2491
|
aspect: aspect > 0 ? aspect : (prev && prev.aspect || 0.772),
|
|
2318
|
-
lines: (prev ? prev.lines.filter((line) => Number(line.page) !== page) : []).concat(body.lines
|
|
2492
|
+
lines: (prev ? prev.lines.filter((line) => Number(line.page) !== page) : []).concat(body.lines.map(line => ({ ...line, page,
|
|
2493
|
+
...(line.ocr !== true && line.precise && Array.isArray(line.glyphs) ? { nativeSelection: true } : {}),
|
|
2494
|
+
}))),
|
|
2319
2495
|
}));
|
|
2320
2496
|
continue;
|
|
2321
2497
|
}
|
|
@@ -2326,7 +2502,10 @@ __define('./reader', (module, exports, require) => {
|
|
|
2326
2502
|
}));
|
|
2327
2503
|
} catch (error) {
|
|
2328
2504
|
if (error && error.name === 'AbortError') return;
|
|
2329
|
-
if (!cancelled)
|
|
2505
|
+
if (!cancelled) {
|
|
2506
|
+
ocrTried.current[token] = 'done';
|
|
2507
|
+
if (format === 'pptx') setPageTextNotice({ page, text: normalizeError(error) });
|
|
2508
|
+
}
|
|
2330
2509
|
}
|
|
2331
2510
|
}
|
|
2332
2511
|
})();
|
|
@@ -2337,7 +2516,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
2337
2516
|
if (ocrTried.current[tokens[i]] === 'pending') delete ocrTried.current[tokens[i]];
|
|
2338
2517
|
}
|
|
2339
2518
|
};
|
|
2340
|
-
}, [format, showLayout, slide, pageModel, pageFailed, rasterPages, p.projectId, p.path, previewRev]);
|
|
2519
|
+
}, [format, showLayout, slide, pageModel, pageFailed, rasterPages, p.projectId, p.path, previewRev, visionModelKey]);
|
|
2341
2520
|
|
|
2342
2521
|
const boundLines = React.useMemo(
|
|
2343
2522
|
() => (format === 'docx' ? bindWordLayoutLines : bindLayoutLines)(blocks, pageModel && pageModel.lines),
|
|
@@ -2781,14 +2960,15 @@ __define('./reader', (module, exports, require) => {
|
|
|
2781
2960
|
if (target && typeof target.closest === 'function' && target.closest('button')) return;
|
|
2782
2961
|
if (startPageGesture(event, event.currentTarget.closest?.('[data-reader-pages]'))) return;
|
|
2783
2962
|
const native = format === 'docx' && (lines || []).some(line => line.precise && Array.isArray(line.glyphs));
|
|
2784
|
-
|
|
2963
|
+
const mixedSlide = format === 'pptx' && (lines || []).some(line => line.nativeSelection);
|
|
2964
|
+
if (!native && !mixedSlide && target && typeof target.closest === 'function') {
|
|
2785
2965
|
const layer = target.closest('[data-text-layer]');
|
|
2786
2966
|
if (layer && layer.getAttribute('data-ocr') !== '1') {
|
|
2787
2967
|
ocrHold.current = false;
|
|
2788
2968
|
return;
|
|
2789
2969
|
}
|
|
2790
2970
|
}
|
|
2791
|
-
let ocrLines = (lines || []).filter(line => line && (native ? line.precise && Array.isArray(line.glyphs) : line.ocr === true))
|
|
2971
|
+
let ocrLines = (lines || []).filter(line => line && (native ? line.precise && Array.isArray(line.glyphs) : (line.ocr === true || line.nativeSelection)))
|
|
2792
2972
|
.map(line => native ? { ...line, nativeSelection: true } : line);
|
|
2793
2973
|
if (ocrLines.length === 0) return;
|
|
2794
2974
|
const plane = event.currentTarget;
|
|
@@ -3033,6 +3213,18 @@ __define('./reader', (module, exports, require) => {
|
|
|
3033
3213
|
});
|
|
3034
3214
|
};
|
|
3035
3215
|
|
|
3216
|
+
const jumpSlide = () => {
|
|
3217
|
+
const value = slideInput.trim(), number = Number(value), max = slideCountRef.current;
|
|
3218
|
+
if (!max) { setSlideJumpError('页数尚未读取完成,请稍后重试。'); return; }
|
|
3219
|
+
if (!/^\d+$/.test(value) || !Number.isSafeInteger(number) || number < 1 || number > max) {
|
|
3220
|
+
setSlideJumpError(`请输入 1–${max} 之间的整数页码。`);
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
setSlideJumpError('');
|
|
3224
|
+
setSlideInput(String(number));
|
|
3225
|
+
setSlide(number);
|
|
3226
|
+
};
|
|
3227
|
+
|
|
3036
3228
|
const slideMode = showLayout && format === 'pptx';
|
|
3037
3229
|
const pdfLineCount = pageModel && Array.isArray(pageModel.lines) ? pageModel.lines.length : 0;
|
|
3038
3230
|
const ocrLineCount = Object.keys(ocrBound).reduce((sum, key) => sum + (Array.isArray(ocrBound[key]) ? ocrBound[key].length : 0), 0);
|
|
@@ -3071,7 +3263,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
3071
3263
|
return;
|
|
3072
3264
|
}
|
|
3073
3265
|
const target = event.target;
|
|
3074
|
-
if (target && typeof target.closest === 'function' && (target.closest('button') || target.closest('[data-text-layer]'))) return;
|
|
3266
|
+
if (target && typeof target.closest === 'function' && (target.closest('button,input,select,textarea') || target.closest('[data-text-layer]'))) return;
|
|
3075
3267
|
const selected = currentWindow();
|
|
3076
3268
|
const live = selected && typeof selected.getSelection === 'function' ? selected.getSelection() : null;
|
|
3077
3269
|
if (live && !live.isCollapsed) return;
|
|
@@ -3156,7 +3348,25 @@ __define('./reader', (module, exports, require) => {
|
|
|
3156
3348
|
disabled: slideCount === 0 || slide >= slideCount,
|
|
3157
3349
|
onClick: () => turnSlide(1),
|
|
3158
3350
|
}, '›'),
|
|
3159
|
-
h('div', { key: 'count', className: P + 'SlideCount'
|
|
3351
|
+
h('div', { key: 'count', className: P + 'SlideCount', 'data-slide-navigation': '1' }, [
|
|
3352
|
+
h('div', { key: 'controls', className: P + 'SlideCountControls' }, [
|
|
3353
|
+
h('input', {
|
|
3354
|
+
key: 'page', type: 'number', inputMode: 'numeric', min: 1, max: slideCount || undefined, step: 1,
|
|
3355
|
+
value: slideInput, disabled: !slideCount, className: P + 'SlidePageInput',
|
|
3356
|
+
'aria-label': '跳转页码', 'aria-invalid': slideJumpError ? 'true' : undefined, 'data-slide-page-input': '1',
|
|
3357
|
+
onChange: event => { setSlideInput(event.target.value); setSlideJumpError(''); },
|
|
3358
|
+
onKeyDown: event => {
|
|
3359
|
+
if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); jumpSlide(); }
|
|
3360
|
+
else if (event.key === 'Escape') { setSlideInput(String(slide)); setSlideJumpError(''); }
|
|
3361
|
+
},
|
|
3362
|
+
}),
|
|
3363
|
+
h('span', { key: 'total', 'data-slide-total': '1' }, slideCount ? `/ ${slideCount}` : '/ …'),
|
|
3364
|
+
h('button', { key: 'jump', type: 'button', className: P + 'SlideJumpButton', disabled: !slideCount,
|
|
3365
|
+
'aria-label': '跳转到指定页', 'data-slide-jump': '1', onClick: jumpSlide }, '跳转'),
|
|
3366
|
+
]),
|
|
3367
|
+
slideJumpError ? h('div', { key: 'error', className: P + 'SlideJumpError', role: 'alert',
|
|
3368
|
+
'data-slide-jump-error': '1' }, slideJumpError) : null,
|
|
3369
|
+
]),
|
|
3160
3370
|
]) : null;
|
|
3161
3371
|
|
|
3162
3372
|
const shownPages = usePages ? Math.min(pageCount, 80) : 0;
|
|
@@ -3214,6 +3424,12 @@ __define('./reader', (module, exports, require) => {
|
|
|
3214
3424
|
|
|
3215
3425
|
return h('div', { className: P + 'Wrap', ref: wrapRef, 'data-reader-wrap': '1', 'data-reader-view': view }, [
|
|
3216
3426
|
viewBar,
|
|
3427
|
+
sourceFormat === 'doc' ? h('div', { key: 'legacy-word', className: P + 'SelectionNotice', role: 'status', 'data-legacy-word-notice': '1' }, [
|
|
3428
|
+
p.convertingWord ? h(Processing, { key: 'progress', text: '正在转换为可修订的 Word 文档…' })
|
|
3429
|
+
: h('span', { key: 'text' }, '旧版 DOC 可以阅读和提问;转换为 DOCX 副本后即可润色、扩写和接受修订。'),
|
|
3430
|
+
h('button', { key: 'convert', type: 'button', className: P + 'Btn', 'data-convert-word': '1',
|
|
3431
|
+
disabled: p.convertingWord || p.conversionDisabled, onClick: p.onConvertWord }, '转换并编辑'),
|
|
3432
|
+
]) : null,
|
|
3217
3433
|
p.officeNotice ? h('div', { key: 'office', className: P + 'SelectionNotice', role: 'alert', 'data-office-notice': '1' }, [
|
|
3218
3434
|
h('span', { key: 'message' }, p.officeNotice.message),
|
|
3219
3435
|
p.officeNotice.helpUrl ? h('a', { key: 'help', href: '/doc-assistant/office/help', target: '_blank', rel: 'noopener noreferrer' }, ' 查看安装指引') : null,
|
|
@@ -3224,6 +3440,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
3224
3440
|
showLayout && p.selectionNotice
|
|
3225
3441
|
? h('div', { key: 'selection-notice', className: P + 'SelectionNotice', role: 'status', 'data-layout-selection-notice': '1' }, p.selectionNotice)
|
|
3226
3442
|
: null,
|
|
3443
|
+
showLayout && format === 'pptx' && pageTextNotice?.page === slide
|
|
3444
|
+
? h('div', { key: 'page-text-notice', className: P + 'SelectionNotice', role: 'status', 'data-page-text-status': '1' },
|
|
3445
|
+
pageTextNotice.pending ? h(Processing, { text: pageTextNotice.text }) : pageTextNotice.text) : null,
|
|
3227
3446
|
findBar,
|
|
3228
3447
|
h('main', {
|
|
3229
3448
|
key: 'main',
|
|
@@ -3500,6 +3719,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
3500
3719
|
const [rightOpen, setRightOpen] = React.useState(true);
|
|
3501
3720
|
const [status, setStatus] = React.useState('');
|
|
3502
3721
|
const [busyAction, setBusyAction] = React.useState('');
|
|
3722
|
+
const [acceptingRevisionId, setAcceptingRevisionId] = React.useState('');
|
|
3503
3723
|
const [revisionTick, setRevisionTick] = React.useState(0);
|
|
3504
3724
|
// 接受写回后幻灯片图的地址要变,否则浏览器会继续显示缓存的旧页。
|
|
3505
3725
|
const [previewEpoch, setPreviewEpoch] = React.useState(0);
|
|
@@ -3512,6 +3732,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
3512
3732
|
const readerStateRef = React.useRef(null);
|
|
3513
3733
|
const reloadDocumentRef = React.useRef(null);
|
|
3514
3734
|
const [officeBusy, setOfficeBusy] = React.useState(false);
|
|
3735
|
+
const [legacyConverting, setLegacyConverting] = React.useState(false);
|
|
3736
|
+
const legacyConvertRef = React.useRef(null);
|
|
3515
3737
|
const officeLaunchRef = React.useRef('');
|
|
3516
3738
|
const [officeNotice, setOfficeNotice] = React.useState(null);
|
|
3517
3739
|
const [booted, setBooted] = React.useState(false);
|
|
@@ -3533,16 +3755,26 @@ __define('./reader', (module, exports, require) => {
|
|
|
3533
3755
|
const [readerView, setReaderView] = React.useState('text');
|
|
3534
3756
|
const [selectionNotice, setSelectionNotice] = React.useState('');
|
|
3535
3757
|
const [noChangeNotice, setNoChangeNotice] = React.useState('');
|
|
3536
|
-
|
|
3537
|
-
|
|
3758
|
+
// Every request owns its immutable selection/model snapshot. Updating a job
|
|
3759
|
+
// replaces the Map so independent completions cannot overwrite each other.
|
|
3760
|
+
const [generationJobs, setGenerationJobs] = React.useState(() => new Map());
|
|
3761
|
+
const generationJobsRef = React.useRef(new Map());
|
|
3762
|
+
const generationSequenceRef = React.useRef(0);
|
|
3763
|
+
const generationAliveRef = React.useRef(true);
|
|
3764
|
+
const writeBusyRef = React.useRef(false);
|
|
3765
|
+
const [revisionErrors, setRevisionErrors] = React.useState(() => new Map());
|
|
3538
3766
|
const generationTargetRef = React.useRef('');
|
|
3539
3767
|
generationTargetRef.current = `${projectId}\u0000${docPath}`;
|
|
3540
|
-
React.
|
|
3768
|
+
const updateGenerationJobs = React.useCallback(update => {
|
|
3769
|
+
const next = update(generationJobsRef.current);
|
|
3770
|
+
generationJobsRef.current = next;
|
|
3771
|
+
if (generationAliveRef.current) setGenerationJobs(next);
|
|
3772
|
+
}, []);
|
|
3541
3773
|
React.useEffect(() => {
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
}, [projectId, docPath]);
|
|
3774
|
+
generationAliveRef.current = true;
|
|
3775
|
+
return () => { generationAliveRef.current = false; };
|
|
3776
|
+
}, []);
|
|
3777
|
+
React.useEffect(() => { setNoChangeNotice(''); }, [projectId, docPath]);
|
|
3546
3778
|
const [layoutPage, setLayoutPage] = React.useState(0);
|
|
3547
3779
|
const [layoutFocus, setLayoutFocus] = React.useState(null);
|
|
3548
3780
|
const [readPct, setReadPct] = React.useState(0);
|
|
@@ -3552,6 +3784,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
3552
3784
|
const [shelfDialogOpen, setShelfDialogOpen] = React.useState(false);
|
|
3553
3785
|
/** 书架重新拉数据的信号(上传 / 生成封面后 +1)。 */
|
|
3554
3786
|
const [shelfReloadKey, setShelfReloadKey] = React.useState(0);
|
|
3787
|
+
const [shelfSummary, setShelfSummary] = React.useState({ projects: 0, documents: 0, loading: true });
|
|
3555
3788
|
const [uploadProgress, setUploadProgress] = React.useState(null);
|
|
3556
3789
|
const [uploadOutcome, setUploadOutcome] = React.useState(null);
|
|
3557
3790
|
const uploadBusyRef = React.useRef(false);
|
|
@@ -3563,6 +3796,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
3563
3796
|
const [messages, setMessages] = React.useState([]);
|
|
3564
3797
|
const [chatBusy, setChatBusy] = React.useState(false);
|
|
3565
3798
|
const [chatKind, setChatKind] = React.useState('chat');
|
|
3799
|
+
const [chatProgress, setChatProgress] = React.useState('');
|
|
3800
|
+
const chatStartedRef = React.useRef(0);
|
|
3566
3801
|
const [chatError, setChatError] = React.useState('');
|
|
3567
3802
|
const [chatModels, setChatModels] = React.useState([]);
|
|
3568
3803
|
const [chatDefaultModel, setChatDefaultModel] = React.useState(null);
|
|
@@ -3605,7 +3840,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
3605
3840
|
// Word / PPT / PDF 打开即版式;Excel 打开即表格文本,版式按需再转。
|
|
3606
3841
|
React.useEffect(() => {
|
|
3607
3842
|
const fmt = doc && doc.format;
|
|
3608
|
-
if (fmt === 'docx' || fmt === 'pptx' || fmt === 'pdf') setReaderView('layout');
|
|
3843
|
+
if (fmt === 'doc' || fmt === 'docx' || fmt === 'pptx' || fmt === 'pdf') setReaderView('layout');
|
|
3609
3844
|
else setReaderView('text');
|
|
3610
3845
|
setLayoutPage(0);
|
|
3611
3846
|
setLayoutFocus(null);
|
|
@@ -3738,7 +3973,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
3738
3973
|
uploadBusyRef.current = true;
|
|
3739
3974
|
setUploadOutcome(null);
|
|
3740
3975
|
setStatus(`正在导入 ${list.length} 个文件…`);
|
|
3741
|
-
let done = 0;
|
|
3976
|
+
let done = 0, converted = 0;
|
|
3742
3977
|
let currentFile = '';
|
|
3743
3978
|
try {
|
|
3744
3979
|
for (const file of list) {
|
|
@@ -3747,11 +3982,12 @@ __define('./reader', (module, exports, require) => {
|
|
|
3747
3982
|
setUploadProgress({ ...progress, stage: '正在读取文件…' });
|
|
3748
3983
|
const dataBase64 = await shelfModule.readFileBase64(file);
|
|
3749
3984
|
// The response also waits for best-effort cover preparation.
|
|
3750
|
-
setUploadProgress({ ...progress, stage: '正在保存文档并准备封面,请稍候…' });
|
|
3751
|
-
await transport.uploadDocs(targetProject, '', [{ name: file.name, dataBase64 }]);
|
|
3985
|
+
setUploadProgress({ ...progress, stage: /\.doc$/i.test(file.name) ? '正在转换为可修订的 Word 文档并准备封面…' : '正在保存文档并准备封面,请稍候…' });
|
|
3986
|
+
const imported = await transport.uploadDocs(targetProject, '', [{ name: file.name, dataBase64 }]);
|
|
3987
|
+
converted += (Array.isArray(imported) ? imported : imported?.entries || []).filter(entry => entry.convertedFrom === 'doc').length;
|
|
3752
3988
|
done += 1;
|
|
3753
3989
|
}
|
|
3754
|
-
const message = `已导入 ${done}
|
|
3990
|
+
const message = `已导入 ${done} 个文件${converted ? `,其中 ${converted} 个 DOC 已转换为可修订的 DOCX` : ''}`;
|
|
3755
3991
|
setStatus(message);
|
|
3756
3992
|
setUploadOutcome({ message, error: false });
|
|
3757
3993
|
} catch (error) {
|
|
@@ -3847,6 +4083,21 @@ __define('./reader', (module, exports, require) => {
|
|
|
3847
4083
|
if (draftReference && rightOpen && rightTab === 'chat' && !chatBusy && chatInputRef.current) chatInputRef.current.focus();
|
|
3848
4084
|
}, [draftReference, rightOpen, rightTab, chatBusy]);
|
|
3849
4085
|
|
|
4086
|
+
React.useEffect(() => {
|
|
4087
|
+
if (!chatBusy || !projectId || !docPath || typeof transport.contextStatus !== 'function' || !chatStartedRef.current) return undefined;
|
|
4088
|
+
let alive = true, timer;
|
|
4089
|
+
const started = chatStartedRef.current;
|
|
4090
|
+
const poll = async () => {
|
|
4091
|
+
try {
|
|
4092
|
+
const state = await transport.contextStatus(projectId, docPath);
|
|
4093
|
+
if (alive && Number(state?.at) >= started && ['reading', 'vision', 'ocr', 'ready'].includes(state.stage)) setChatProgress(state.message || '');
|
|
4094
|
+
} catch { /* The original chat request reports any real failure. */ }
|
|
4095
|
+
if (alive) timer = setTimeout(poll, 1000);
|
|
4096
|
+
};
|
|
4097
|
+
poll();
|
|
4098
|
+
return () => { alive = false; clearTimeout(timer); };
|
|
4099
|
+
}, [chatBusy, projectId, docPath, transport]);
|
|
4100
|
+
|
|
3850
4101
|
// ---- 新消息滚到底
|
|
3851
4102
|
React.useEffect(() => {
|
|
3852
4103
|
const el = chatListRef.current;
|
|
@@ -3880,6 +4131,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
3880
4131
|
setDraft('');
|
|
3881
4132
|
setDraftReference(null);
|
|
3882
4133
|
}
|
|
4134
|
+
chatStartedRef.current = Date.now();
|
|
4135
|
+
setChatProgress('');
|
|
3883
4136
|
setChatBusy(true);
|
|
3884
4137
|
setChatKind(kind);
|
|
3885
4138
|
setChatError('');
|
|
@@ -3918,6 +4171,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
3918
4171
|
if (chatInFlightRef.current && chatInFlightRef.current.target === target) return;
|
|
3919
4172
|
const request = ++chatRequestRef.current;
|
|
3920
4173
|
chatInFlightRef.current = { target, request };
|
|
4174
|
+
chatStartedRef.current = 0; setChatProgress('');
|
|
3921
4175
|
setChatBusy(true);
|
|
3922
4176
|
setChatError('');
|
|
3923
4177
|
try {
|
|
@@ -3982,57 +4236,66 @@ __define('./reader', (module, exports, require) => {
|
|
|
3982
4236
|
const payload = extra?.request ? { ...extra.request } : { projectId, path: docPath, anchor, instruction, action: actionId };
|
|
3983
4237
|
if (!extra?.request && typeof extra?.revised !== 'string') Object.assign(payload, chosenModel(chatModel));
|
|
3984
4238
|
if (!extra?.request && doc && doc.format === 'docx') {
|
|
3985
|
-
|
|
3986
|
-
payload.expectedOriginal = source.slice(anchor.srcStart, anchor.srcEnd);
|
|
4239
|
+
payload.expectedOriginal = wordAnchorText(doc, anchor);
|
|
3987
4240
|
if (doc.meta?.documentHash) payload.expectedDocumentHash = doc.meta.documentHash;
|
|
3988
4241
|
}
|
|
3989
4242
|
if (extra && typeof extra.revised === 'string') payload.revised = extra.revised;
|
|
3990
4243
|
const snapshot = captureRequest(payload);
|
|
3991
4244
|
const target = `${snapshot.projectId}\u0000${snapshot.path}`;
|
|
3992
|
-
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
4245
|
+
const range = snapshot.anchor || {};
|
|
4246
|
+
const ranges = Array.isArray(range.segments) ? range.segments : [range];
|
|
4247
|
+
const key = JSON.stringify([target, snapshot.action || actionId,
|
|
4248
|
+
snapshot.expectedDocumentHash || doc?.meta?.documentHash, snapshot.expectedOriginal ?? wordAnchorText(doc, snapshot.anchor), ranges.map(part => [part.kind,
|
|
4249
|
+
part.srcStart, part.srcEnd, part.blockId, part.sheet, part.cell, part.row, part.col])]);
|
|
4250
|
+
if (Array.from(generationJobsRef.current.values()).some(job => job.key === key && job.state === 'running')) return;
|
|
4251
|
+
const id = extra?.jobId || `generation-${++generationSequenceRef.current}`;
|
|
4252
|
+
const operation = Object.freeze({ id, key, target, snapshot, state: 'running', replaceId: extra?.replaceId,
|
|
4253
|
+
original: snapshot.expectedOriginal ?? wordAnchorText(doc, snapshot.anchor),
|
|
4254
|
+
documentHash: snapshot.expectedDocumentHash || doc?.meta?.documentHash,
|
|
4255
|
+
label: actionId === 'expand' ? '扩写' : actionId === 'manual' ? '修改' : '润色' });
|
|
4256
|
+
updateGenerationJobs(previous => { const next = new Map(previous); next.set(id, operation); return next; });
|
|
4257
|
+
const isCurrent = () => generationAliveRef.current && generationTargetRef.current === target
|
|
4258
|
+
&& store.projectId === snapshot.projectId && store.docPath === snapshot.path;
|
|
4259
|
+
const finishJob = changes => updateGenerationJobs(previous => {
|
|
4260
|
+
if (previous.get(id) !== operation) return previous;
|
|
4261
|
+
const next = new Map(previous);
|
|
4262
|
+
if (changes) next.set(id, Object.freeze({ ...operation, ...changes }));
|
|
4263
|
+
else next.delete(id);
|
|
4264
|
+
return next;
|
|
4265
|
+
});
|
|
3996
4266
|
let replaced = false;
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
// 生成后让用户直接看到结果:展开右栏并切到「修订」标签。
|
|
4003
|
-
setRightOpen(true);
|
|
4004
|
-
setRightTab('revise');
|
|
4267
|
+
if (isCurrent()) {
|
|
4268
|
+
setStatus(extra?.replaceId ? '正在重新生成…' : '正在生成修订…');
|
|
4269
|
+
setSelectionNotice(''); setNoChangeNotice('');
|
|
4270
|
+
setRightOpen(true); setRightTab('revise');
|
|
4271
|
+
}
|
|
4005
4272
|
try {
|
|
4006
4273
|
if (extra?.replaceId) {
|
|
4007
4274
|
await store.delete(extra.replaceId, { optimistic: true });
|
|
4008
4275
|
replaced = true;
|
|
4009
4276
|
}
|
|
4010
4277
|
const revision = await store.generate(snapshot);
|
|
4011
|
-
if (!isCurrent()) return revision;
|
|
4012
4278
|
if (revision?.unchanged) {
|
|
4013
|
-
const message = `${
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
setStatus(message);
|
|
4017
|
-
}
|
|
4279
|
+
const message = `${operation.label}结果与原文一致,内容无变化。`;
|
|
4280
|
+
finishJob({ state: 'unchanged', message, replaceId: undefined });
|
|
4281
|
+
if (isCurrent() && !extra?.batch) setStatus(message);
|
|
4018
4282
|
return revision;
|
|
4019
4283
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4284
|
+
finishJob(null);
|
|
4285
|
+
if (isCurrent()) {
|
|
4286
|
+
const note = extra && extra.note ? `${extra.note} ` : '';
|
|
4287
|
+
setStatus(`${note}${revision && revision.id ? `已生成修订 ${revision.id}` : '已生成修订'}`);
|
|
4288
|
+
}
|
|
4022
4289
|
return revision;
|
|
4023
4290
|
} catch (error) {
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
setStatus(message);
|
|
4027
|
-
setGenerationError({ message, request: snapshot, replaceId: !replaced ? extra?.replaceId : undefined });
|
|
4028
|
-
} finally {
|
|
4029
|
-
if (isCurrent()) setBusyAction('');
|
|
4030
|
-
if (generationRef.current === operation) generationRef.current = null;
|
|
4291
|
+
const message = `${extra?.replaceId ? '重新生成' : operation.label}失败:${normalizeError(error)}`;
|
|
4292
|
+
finishJob({ state: 'failed', message, replaceId: !replaced ? extra?.replaceId : undefined });
|
|
4293
|
+
if (isCurrent()) setStatus(message);
|
|
4031
4294
|
}
|
|
4032
|
-
}, [store, projectId, docPath, doc, chatModel]);
|
|
4295
|
+
}, [store, projectId, docPath, doc, chatModel, updateGenerationJobs]);
|
|
4033
4296
|
|
|
4034
4297
|
const refreshDoc = React.useCallback(() => {
|
|
4035
|
-
if (docPath) openDoc(docPath, { keepStatus: true });
|
|
4298
|
+
if (docPath) return openDoc(docPath, { keepStatus: true });
|
|
4036
4299
|
}, [docPath, openDoc]);
|
|
4037
4300
|
|
|
4038
4301
|
const reloadDocument = async (automatic = false) => {
|
|
@@ -4081,83 +4344,86 @@ __define('./reader', (module, exports, require) => {
|
|
|
4081
4344
|
};
|
|
4082
4345
|
|
|
4083
4346
|
const acceptRevision = React.useCallback(async (revision) => {
|
|
4084
|
-
if (!revision) return;
|
|
4085
|
-
|
|
4347
|
+
if (!revision || writeBusyRef.current) return;
|
|
4348
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4349
|
+
const isCurrent = () => generationTargetRef.current === target && `${store.projectId}\u0000${store.docPath}` === target;
|
|
4350
|
+
writeBusyRef.current = true; setBusyAction(`rev:${revision.id}`); setAcceptingRevisionId(revision.id);
|
|
4351
|
+
setStatus(/\.docx$/i.test(revision.docPath || '') ? '正在核对版式并写回…' : '正在写回修订…');
|
|
4352
|
+
setRevisionErrors(previous => { const next = new Map(previous); next.delete(revision.id); return next; });
|
|
4086
4353
|
try {
|
|
4087
|
-
const out = await store.accept(revision.id
|
|
4354
|
+
const out = await store.accept(revision.id, revision.layoutWarning?.token
|
|
4355
|
+
? { layoutRiskToken: revision.layoutWarning.token } : undefined);
|
|
4356
|
+
if (!isCurrent()) return;
|
|
4088
4357
|
const written = out && out.written;
|
|
4089
|
-
setStatus(written
|
|
4090
|
-
?
|
|
4091
|
-
: '已接受修订');
|
|
4092
|
-
setPreviewEpoch(
|
|
4093
|
-
refreshDoc();
|
|
4358
|
+
setStatus(written?.layoutRiskAccepted
|
|
4359
|
+
? '已接受并写入修订,请点击文档标题右侧的 Office 图标,手动打开文档调整排版。'
|
|
4360
|
+
: written && written.backup ? `已接受并写回(${written.bytes || 0} 字节,备份:${written.backup})` : '已接受修订');
|
|
4361
|
+
setPreviewEpoch(value => value + 1);
|
|
4362
|
+
await Promise.all([refreshDoc(), store.load()]);
|
|
4094
4363
|
} catch (error) {
|
|
4095
|
-
|
|
4364
|
+
if (!isCurrent()) return;
|
|
4365
|
+
const message = normalizeError(error);
|
|
4366
|
+
setStatus(`${error?.body?.revision?.layoutWarning ? '排版提示' : '接受失败'}:${message}`);
|
|
4367
|
+
setRevisionErrors(previous => { const next = new Map(previous); next.set(revision.id, message); return next; });
|
|
4096
4368
|
} finally {
|
|
4097
|
-
setBusyAction('');
|
|
4369
|
+
writeBusyRef.current = false; setBusyAction(''); setAcceptingRevisionId('');
|
|
4098
4370
|
}
|
|
4099
4371
|
}, [store, refreshDoc]);
|
|
4100
4372
|
|
|
4101
4373
|
const rejectRevision = React.useCallback(async (revision) => {
|
|
4102
4374
|
if (!revision) return;
|
|
4103
|
-
|
|
4375
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4104
4376
|
try {
|
|
4105
4377
|
await store.reject(revision.id);
|
|
4106
|
-
setStatus('已撤回修订');
|
|
4378
|
+
if (generationTargetRef.current === target) setStatus('已撤回修订');
|
|
4107
4379
|
} catch (error) {
|
|
4108
|
-
setStatus(`撤回失败:${normalizeError(error)}`);
|
|
4109
|
-
} finally {
|
|
4110
|
-
setBusyAction('');
|
|
4380
|
+
if (generationTargetRef.current === target) setStatus(`撤回失败:${normalizeError(error)}`);
|
|
4111
4381
|
}
|
|
4112
4382
|
}, [store]);
|
|
4113
4383
|
|
|
4114
4384
|
const deleteRevision = React.useCallback(async (revision) => {
|
|
4115
4385
|
if (!revision) return;
|
|
4116
|
-
|
|
4386
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4117
4387
|
try {
|
|
4118
|
-
await store.delete(revision.id);
|
|
4119
|
-
setStatus(revision.status === 'accepted'
|
|
4388
|
+
await store.delete(revision.id, { optimistic: true });
|
|
4389
|
+
if (generationTargetRef.current === target) setStatus(revision.status === 'accepted'
|
|
4390
|
+
? '已删除修订卡片,文档中已写入的修改保留。' : '已删除修订卡片');
|
|
4120
4391
|
} catch (error) {
|
|
4121
|
-
setStatus(`删除修订失败:${normalizeError(error)}`);
|
|
4122
|
-
}
|
|
4392
|
+
if (generationTargetRef.current === target) setStatus(`删除修订失败:${normalizeError(error)}`);
|
|
4393
|
+
}
|
|
4123
4394
|
}, [store]);
|
|
4124
4395
|
|
|
4125
4396
|
const restoreRevision = React.useCallback(async (revision) => {
|
|
4126
|
-
if (!revision) return;
|
|
4127
|
-
|
|
4397
|
+
if (!revision || writeBusyRef.current) return;
|
|
4398
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4399
|
+
writeBusyRef.current = true; setBusyAction(`rev:${revision.id}`);
|
|
4128
4400
|
try {
|
|
4129
4401
|
await store.restore(revision.id);
|
|
4402
|
+
if (generationTargetRef.current !== target) return;
|
|
4130
4403
|
setStatus(revision.status === 'accepted' ? '已撤销写入并还原文件' : '已恢复为待处理');
|
|
4131
|
-
setPreviewEpoch(
|
|
4132
|
-
refreshDoc();
|
|
4133
|
-
await store.load();
|
|
4404
|
+
setPreviewEpoch(value => value + 1);
|
|
4405
|
+
await Promise.all([refreshDoc(), store.load()]);
|
|
4134
4406
|
} catch (error) {
|
|
4135
|
-
setStatus(`恢复失败:${normalizeError(error)}`);
|
|
4136
|
-
} finally {
|
|
4137
|
-
setBusyAction('');
|
|
4138
|
-
}
|
|
4407
|
+
if (generationTargetRef.current === target) setStatus(`恢复失败:${normalizeError(error)}`);
|
|
4408
|
+
} finally { writeBusyRef.current = false; setBusyAction(''); }
|
|
4139
4409
|
}, [store, refreshDoc]);
|
|
4140
4410
|
|
|
4141
4411
|
const restoreWriteGroup = React.useCallback(async (group) => {
|
|
4142
|
-
const list = Array.isArray(group) ? group.filter(
|
|
4143
|
-
if (list.length
|
|
4144
|
-
|
|
4412
|
+
const list = Array.isArray(group) ? group.filter(item => item && item.id) : [];
|
|
4413
|
+
if (!list.length || writeBusyRef.current) return;
|
|
4414
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4415
|
+
writeBusyRef.current = true; setBusyAction(`rev:${list[0].id}`);
|
|
4145
4416
|
try {
|
|
4146
|
-
const results = await store.bulk(list.map(
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
return;
|
|
4151
|
-
}
|
|
4417
|
+
const results = await store.bulk(list.map(item => item.id), 'restore');
|
|
4418
|
+
if (generationTargetRef.current !== target) return;
|
|
4419
|
+
const failed = (results || []).filter(item => item && item.ok === false);
|
|
4420
|
+
if (failed.length) { setStatus(`整组撤回失败:${failed[0].error || ''}`); return; }
|
|
4152
4421
|
setStatus(`已撤回本次批量写入(${list.length} 条)`);
|
|
4153
|
-
setPreviewEpoch(
|
|
4154
|
-
refreshDoc();
|
|
4155
|
-
await store.load();
|
|
4422
|
+
setPreviewEpoch(value => value + 1);
|
|
4423
|
+
await Promise.all([refreshDoc(), store.load()]);
|
|
4156
4424
|
} catch (error) {
|
|
4157
|
-
setStatus(`整组撤回失败:${normalizeError(error)}`);
|
|
4158
|
-
} finally {
|
|
4159
|
-
setBusyAction('');
|
|
4160
|
-
}
|
|
4425
|
+
if (generationTargetRef.current === target) setStatus(`整组撤回失败:${normalizeError(error)}`);
|
|
4426
|
+
} finally { writeBusyRef.current = false; setBusyAction(''); }
|
|
4161
4427
|
}, [store, refreshDoc]);
|
|
4162
4428
|
|
|
4163
4429
|
const regenerate = React.useCallback(async (revision) => {
|
|
@@ -4176,15 +4442,21 @@ __define('./reader', (module, exports, require) => {
|
|
|
4176
4442
|
}, [generate, projectId, docPath, chatModel]);
|
|
4177
4443
|
|
|
4178
4444
|
const acceptAll = React.useCallback(async () => {
|
|
4445
|
+
if (writeBusyRef.current) return;
|
|
4179
4446
|
if (groupRevisionVariants(store.all()).some(group => group.items.filter(item => item.status === 'pending').length > 1)) {
|
|
4180
4447
|
setStatus('同一选区有多份扩写,请选择其中一份接受。');
|
|
4181
4448
|
return;
|
|
4182
4449
|
}
|
|
4450
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4451
|
+
writeBusyRef.current = true;
|
|
4183
4452
|
setBusyAction('accept-all');
|
|
4184
4453
|
setStatus('正在批量接受…');
|
|
4185
4454
|
try {
|
|
4186
4455
|
const results = await store.acceptAll();
|
|
4456
|
+
if (generationTargetRef.current !== target) return;
|
|
4187
4457
|
const failed = (results || []).filter((item) => item && item.ok === false);
|
|
4458
|
+
if (failed.length) setRevisionErrors(previous => { const next = new Map(previous);
|
|
4459
|
+
failed.forEach(item => next.set(item.revisionId, item.error || '接受失败')); return next; });
|
|
4188
4460
|
const wrote = (results || []).some((item) => item && item.ok !== false);
|
|
4189
4461
|
setStatus(failed.length > 0
|
|
4190
4462
|
? `接受完成,${failed.length} 条失败:${failed[0].error || ''}`
|
|
@@ -4192,9 +4464,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
4192
4464
|
if (wrote) setPreviewEpoch((value) => value + 1);
|
|
4193
4465
|
refreshDoc();
|
|
4194
4466
|
} catch (error) {
|
|
4195
|
-
setStatus(`批量接受失败:${normalizeError(error)}`);
|
|
4467
|
+
if (generationTargetRef.current === target) setStatus(`批量接受失败:${normalizeError(error)}`);
|
|
4196
4468
|
} finally {
|
|
4197
|
-
setBusyAction('');
|
|
4469
|
+
writeBusyRef.current = false; setBusyAction('');
|
|
4198
4470
|
}
|
|
4199
4471
|
}, [store, refreshDoc]);
|
|
4200
4472
|
|
|
@@ -4262,18 +4534,48 @@ __define('./reader', (module, exports, require) => {
|
|
|
4262
4534
|
if (!instruction) return;
|
|
4263
4535
|
// 只读格式即使工具条漏了按钮,也不再打出「改写失败」。
|
|
4264
4536
|
if (!revisionAllowed(doc)) return;
|
|
4537
|
+
const isWordEdit = doc?.format === 'docx' && (actionId === 'polish' || actionId === 'expand');
|
|
4538
|
+
if (isWordEdit && wordListMarker(hit.text)) {
|
|
4539
|
+
setNoChangeNotice('选区只有列表标记,没有需要修改的正文。');
|
|
4540
|
+
setStatus('已保留原列表标记,内容无变化。');
|
|
4541
|
+
return;
|
|
4542
|
+
}
|
|
4265
4543
|
const anchor = hit.anchor || {};
|
|
4266
4544
|
const hasRange = Number.isInteger(anchor.srcStart) && Number.isInteger(anchor.srcEnd);
|
|
4267
4545
|
const parts = Array.isArray(hit.parts) ? hit.parts : [];
|
|
4268
4546
|
const shapeNote = hit.matched === 'shape' ? '识别和原文不一致,已按整个文本框改写。' : '';
|
|
4547
|
+
if (isWordEdit && (parts.length > 1 || Array.isArray(anchor.segments))) {
|
|
4548
|
+
const allParts = parts.length ? parts : (anchor.segments || []).map(segment => ({ anchor: segment,
|
|
4549
|
+
text: wordAnchorText(doc, segment) }));
|
|
4550
|
+
if (!wordCompoundAnchor(doc, allParts)) {
|
|
4551
|
+
cannotLocate(); return;
|
|
4552
|
+
}
|
|
4553
|
+
let firstBody = 0;
|
|
4554
|
+
while (firstBody < allParts.length && wordListMarker(allParts[firstBody].text)) firstBody += 1;
|
|
4555
|
+
const body = allParts.slice(firstBody), combined = wordCompoundAnchor(doc, body);
|
|
4556
|
+
if (!combined || body.some(part => wordListMarker(part.text)) || !continuousWordAnchor(doc, combined)) {
|
|
4557
|
+
const message = '选区中间有未选中的文字或列表标记,请连续选择一段正文后再修改。';
|
|
4558
|
+
setSelectionNotice(message); setStatus(message); return;
|
|
4559
|
+
}
|
|
4560
|
+
// One model request sees the complete selected paragraph. Per-line
|
|
4561
|
+
// rewriting could skip an unchanged first line and fragment the result.
|
|
4562
|
+
generate(combined, instruction, actionId);
|
|
4563
|
+
return;
|
|
4564
|
+
}
|
|
4269
4565
|
if (!hasRange && anchor.kind !== 'cell' && parts.length > 1) {
|
|
4270
|
-
const
|
|
4566
|
+
const located = parts.filter((part) => (
|
|
4271
4567
|
part && part.anchor && Number.isInteger(part.anchor.srcStart) && Number.isInteger(part.anchor.srcEnd)
|
|
4272
4568
|
));
|
|
4273
|
-
if (
|
|
4569
|
+
if (located.length === 0 || (doc && doc.format === 'docx' && located.length < parts.length)) {
|
|
4274
4570
|
cannotLocate();
|
|
4275
4571
|
return;
|
|
4276
4572
|
}
|
|
4573
|
+
const ready = located;
|
|
4574
|
+
if (!ready.length) {
|
|
4575
|
+
setNoChangeNotice('选区只有列表标记,没有需要修改的正文。');
|
|
4576
|
+
setStatus('已保留原列表标记,内容无变化。');
|
|
4577
|
+
return;
|
|
4578
|
+
}
|
|
4277
4579
|
(async () => {
|
|
4278
4580
|
let unchanged = 0, changed = 0, failed = 0;
|
|
4279
4581
|
for (let i = 0; i < ready.length; i += 1) {
|
|
@@ -4287,7 +4589,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4287
4589
|
if (unchanged) setNoChangeNotice(unchanged === ready.length
|
|
4288
4590
|
? '所选内容与原文一致,内容无变化。' : `${unchanged} 段内容无变化,已跳过。`);
|
|
4289
4591
|
setStatus([changed ? `已生成 ${changed} 段修订` : '', unchanged ? `${unchanged} 段内容无变化` : '',
|
|
4290
|
-
failed ? `${failed} 段处理失败` : '',
|
|
4592
|
+
failed ? `${failed} 段处理失败` : '', located.length < parts.length ? `${parts.length - located.length} 段对不上原文` : ''].filter(Boolean).join(';'));
|
|
4291
4593
|
})();
|
|
4292
4594
|
return;
|
|
4293
4595
|
}
|
|
@@ -4366,7 +4668,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4366
4668
|
// ---- 渲染:左栏(§17.6:顺序改为「大纲 → 文档树」,阅读时大纲是第一需要)
|
|
4367
4669
|
const left = (!leftOpen || hideRail) ? null : h('aside', { key: 'left', className: P + 'Left', 'data-reader-rail': '1' }, [
|
|
4368
4670
|
h('div', { key: 'proj', className: P + 'Section' }, [
|
|
4369
|
-
h('div', { key: 't', className: P + 'SectionTitle' }, '
|
|
4671
|
+
h('div', { key: 't', className: P + 'SectionTitle' }, '书架'),
|
|
4370
4672
|
projects.length === 0
|
|
4371
4673
|
? h('div', { key: 'e', className: P + 'Hint' }, '还没有书架。点右上角「+ 新建书架」。')
|
|
4372
4674
|
: h('select', {
|
|
@@ -4423,6 +4725,28 @@ __define('./reader', (module, exports, require) => {
|
|
|
4423
4725
|
: treeNodes('', 0, treeCtx)),
|
|
4424
4726
|
]);
|
|
4425
4727
|
|
|
4728
|
+
const convertLegacyWord = async () => {
|
|
4729
|
+
if (legacyConvertRef.current || docBusy || busyAction || !projectId || !docPath) return;
|
|
4730
|
+
const target = `${projectId}\u0000${docPath}`, operation = { target };
|
|
4731
|
+
legacyConvertRef.current = operation; setLegacyConverting(true); setOfficeNotice(null);
|
|
4732
|
+
setStatus('正在转换为可修订的 Word 文档…');
|
|
4733
|
+
try {
|
|
4734
|
+
const entry = await transport.convertWord(projectId, docPath);
|
|
4735
|
+
if (!entry?.relPath) throw new Error('转换未返回可编辑的 Word 文件');
|
|
4736
|
+
setShelfReloadKey(value => value + 1);
|
|
4737
|
+
if (chatTargetRef.current === target) {
|
|
4738
|
+
await openDoc(entry.relPath, { projectId });
|
|
4739
|
+
setStatus(`已打开可修订副本 ${entry.name || entry.relPath}`);
|
|
4740
|
+
}
|
|
4741
|
+
} catch (error) {
|
|
4742
|
+
if (chatTargetRef.current === target) {
|
|
4743
|
+
setOfficeNotice({ message: normalizeError(error) }); setStatus(`转换失败:${normalizeError(error)}`);
|
|
4744
|
+
}
|
|
4745
|
+
} finally {
|
|
4746
|
+
if (legacyConvertRef.current === operation) { legacyConvertRef.current = null; setLegacyConverting(false); }
|
|
4747
|
+
}
|
|
4748
|
+
};
|
|
4749
|
+
|
|
4426
4750
|
// ---- 渲染:中栏正文
|
|
4427
4751
|
const center = h(DocReader, {
|
|
4428
4752
|
key: 'center',
|
|
@@ -4430,6 +4754,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4430
4754
|
docBusy,
|
|
4431
4755
|
docError,
|
|
4432
4756
|
pendingByBlock,
|
|
4757
|
+
visionRoute: chosenModel(chatModel),
|
|
4433
4758
|
busyAction: busyAction || (chatBusy && chatKind === 'translate' ? 'translate' : ''),
|
|
4434
4759
|
actions: SELECTION_ACTIONS,
|
|
4435
4760
|
projectId,
|
|
@@ -4449,6 +4774,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
4449
4774
|
onAction: runAction,
|
|
4450
4775
|
previewEpoch,
|
|
4451
4776
|
officeNotice,
|
|
4777
|
+
onConvertWord: convertLegacyWord,
|
|
4778
|
+
convertingWord: legacyConverting,
|
|
4779
|
+
conversionDisabled: docBusy || Boolean(busyAction),
|
|
4452
4780
|
onRefresh: reloadDocument,
|
|
4453
4781
|
refreshDisabled: docBusy || Boolean(busyAction),
|
|
4454
4782
|
onAccept: acceptRevision,
|
|
@@ -4528,7 +4856,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4528
4856
|
messageFooter(message),
|
|
4529
4857
|
])),
|
|
4530
4858
|
chatBusy ? h('div', { key: 'thinking', className: P + 'Thinking', 'data-chat-thinking': '1' },
|
|
4531
|
-
h(Processing, { kind: chatKind, text: chatKind === 'translate' ? '正在翻译…' : '思考中…' })) : null,
|
|
4859
|
+
h(Processing, { kind: chatKind, text: chatProgress || (chatKind === 'translate' ? '正在翻译…' : '思考中…') })) : null,
|
|
4532
4860
|
),
|
|
4533
4861
|
chatError ? h('div', { key: 'chatErr', className: P + 'Err', 'data-chat-error': '1' }, chatError) : null,
|
|
4534
4862
|
h('div', { key: 'composer', className: P + 'Composer' }, [
|
|
@@ -4590,7 +4918,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4590
4918
|
h('option', { key: 'default', value: '' }, chatModelsBusy ? '加载模型…' : `跟随 DSH${chatDefaultModel ? ` · ${chatDefaultModel.model}` : ''}`),
|
|
4591
4919
|
chatModel && !chatModels.some(item => JSON.stringify([item.provider, item.model]) === chatModel)
|
|
4592
4920
|
? h('option', { key: 'unavailable', value: chatModel }, '所选模型暂不可用') : null,
|
|
4593
|
-
...chatModels.map(item => h('option', { key: JSON.stringify([item.provider, item.model]), value: JSON.stringify([item.provider, item.model]) }, `${item.label} · ${item.providerLabel}`)),
|
|
4921
|
+
...chatModels.map(item => h('option', { key: JSON.stringify([item.provider, item.model]), value: JSON.stringify([item.provider, item.model]) }, `${item.label} · ${item.providerLabel}${item.inputModalities?.includes('image') ? ' · 图片' : ''}`)),
|
|
4594
4922
|
]),
|
|
4595
4923
|
]),
|
|
4596
4924
|
h('div', { key: 'hint', className: P + 'Hint' },
|
|
@@ -4606,7 +4934,11 @@ __define('./reader', (module, exports, require) => {
|
|
|
4606
4934
|
collapsed: collapsedRevisions.includes(item.id),
|
|
4607
4935
|
onToggleCollapsed: id => setCollapsedRevisions(previous => previous.includes(id)
|
|
4608
4936
|
? previous.filter(value => value !== id) : previous.concat(id).slice(-500)),
|
|
4609
|
-
busy:
|
|
4937
|
+
busy: store.isBusy(`rev:${item.id}`),
|
|
4938
|
+
writeBusy: Boolean(busyAction),
|
|
4939
|
+
acceptError: revisionErrors.get(item.id),
|
|
4940
|
+
progress: acceptingRevisionId === item.id ? h(Processing, { kind: 'accept',
|
|
4941
|
+
text: /\.docx$/i.test(item.docPath || '') ? '正在核对版式并写回…' : '正在写回修订…' }) : null,
|
|
4610
4942
|
writeGroup: (revision) => store.writeGroup(revision),
|
|
4611
4943
|
onAccept: acceptRevision,
|
|
4612
4944
|
onReject: rejectRevision,
|
|
@@ -4624,16 +4956,26 @@ __define('./reader', (module, exports, require) => {
|
|
|
4624
4956
|
store.loading ? h('div', { className: P + 'Hint' }, '加载修订中…') : null,
|
|
4625
4957
|
store.error ? h('div', { className: P + 'Err' }, store.error) : null,
|
|
4626
4958
|
noChangeNotice ? h('div', { className: P + 'NoChange', role: 'status', 'data-revision-unchanged': '1' }, noChangeNotice) : null,
|
|
4627
|
-
|
|
4628
|
-
h('div', { key:
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4959
|
+
Array.from(generationJobs.values()).filter(job => job.target === generationTargetRef.current).reverse().map(job =>
|
|
4960
|
+
h('div', { key: job.id, className: P + 'RevCard', 'data-generation-id': job.id,
|
|
4961
|
+
'data-generation-state': job.state, 'data-generation-action': job.snapshot.action,
|
|
4962
|
+
'data-generation-error': job.state === 'failed' ? '1' : undefined,
|
|
4963
|
+
role: job.state === 'failed' ? 'alert' : 'status' }, [
|
|
4964
|
+
h('div', { key: 'label', className: P + 'RevCollapse' }, `${job.label} · ${anchorLabel(job.snapshot.anchor)}`),
|
|
4965
|
+
typeof job.original === 'string' ? h('div', { key: 'original', className: P + 'GenerationOriginal',
|
|
4966
|
+
title: job.original }, job.original) : null,
|
|
4967
|
+
job.state === 'running' ? h(Processing, { key: 'progress', kind: job.snapshot.action,
|
|
4968
|
+
text: `正在${job.label}…` }) : null,
|
|
4969
|
+
job.message ? h('div', { key: 'message', className: job.state === 'failed' ? P + 'Err' : P + 'NoChange',
|
|
4970
|
+
'data-revision-unchanged': job.state === 'unchanged' ? '1' : undefined }, job.message) : null,
|
|
4971
|
+
job.state === 'failed' ? h('button', { key: 'retry', type: 'button', className: P + 'Btn',
|
|
4972
|
+
onClick: () => generate(job.snapshot.anchor, job.snapshot.instruction, job.snapshot.action,
|
|
4973
|
+
{ request: job.snapshot, replaceId: job.replaceId, jobId: job.id }) }, '重试') : null,
|
|
4974
|
+
job.state !== 'running' ? h('button', { key: 'dismiss', type: 'button', className: P + 'Btn',
|
|
4975
|
+
onClick: () => updateGenerationJobs(previous => { const next = new Map(previous); next.delete(job.id); return next; }) }, '删除') : null,
|
|
4976
|
+
])),
|
|
4977
|
+
items.length === 0 && !store.loading && !noChangeNotice
|
|
4978
|
+
&& !Array.from(generationJobs.values()).some(job => job.target === generationTargetRef.current)
|
|
4637
4979
|
? h('div', { className: P + 'Hint' }, revisionAllowed(doc)
|
|
4638
4980
|
? '选中正文后点「润色 / 扩写」生成修订;接受才会写回文件。'
|
|
4639
4981
|
: '这份文档只能阅读,不能生成写回修订。')
|
|
@@ -4733,6 +5075,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
4733
5075
|
]);
|
|
4734
5076
|
|
|
4735
5077
|
const currentProject = projects.find((item) => item && item.id === projectId) || null;
|
|
5078
|
+
const shelfSummaryText = shelfSummary.loading && shelfSummary.projects === 0
|
|
5079
|
+
? '正在载入书架…' : `${shelfSummary.projects} 个书架 · ${shelfSummary.documents} 份文档`;
|
|
4736
5080
|
|
|
4737
5081
|
// ---- 渲染:顶栏(标题 + + 新建书架 + 右上角齿轮),§17.5
|
|
4738
5082
|
const topBar = h('div', { key: 'top', className: cx(P + 'TopBar', P + 'DragBar'), 'data-shell-top': '1', 'data-window-drag': true, 'data-window-drag-bar': 'title' }, [
|
|
@@ -4752,6 +5096,10 @@ __define('./reader', (module, exports, require) => {
|
|
|
4752
5096
|
onClick: () => setLeftOpen(value => !value),
|
|
4753
5097
|
}, h(PanelLeftIcon, { flipped: !leftOpen })) : null,
|
|
4754
5098
|
h('span', { key: 'title', className: P + 'TopTitle', 'data-window-drag': true }, '企业文档助手'),
|
|
5099
|
+
screen === 'shelf' ? h('span', {
|
|
5100
|
+
key: 'shelf-summary', className: P + 'TopCrumb', 'data-shelf-summary': '1',
|
|
5101
|
+
title: shelfSummaryText, 'data-window-drag': true, 'aria-live': 'polite',
|
|
5102
|
+
}, h('span', { className: P + 'BarTitle' }, shelfSummaryText)) : null,
|
|
4755
5103
|
screen === 'reader' && docName !== '未选择文档'
|
|
4756
5104
|
? h('span', { key: 'crumb', className: P + 'TopCrumb', title: docName, 'data-window-drag': true }, [
|
|
4757
5105
|
h('span', { key: 'dot' }, '· '), h('span', { key: 'name', className: P + 'BarTitle' }, docName),
|
|
@@ -4767,6 +5115,14 @@ __define('./reader', (module, exports, require) => {
|
|
|
4767
5115
|
h('path', { key: 'pen', d: 'm7 9 1.5-.3L14 3.2 12.8 2 7.3 7.5 7 9Z' }),
|
|
4768
5116
|
])) : null,
|
|
4769
5117
|
h('div', { key: 'grow', className: P + 'BarSpacer', 'data-window-drag': true, 'data-title-drag-blank': true, 'aria-hidden': true }),
|
|
5118
|
+
screen === 'shelf' && uploadText ? h('span', {
|
|
5119
|
+
key: 'shelf-busy', className: P + 'TopBusy', 'data-shelf-busy': '1',
|
|
5120
|
+
}, uploadText) : null,
|
|
5121
|
+
screen === 'shelf' ? h('button', {
|
|
5122
|
+
key: 'shelf-refresh', type: 'button', className: P + 'Btn', 'data-shelf-reload': '1',
|
|
5123
|
+
title: '重新载入书架', disabled: shelfSummary.loading,
|
|
5124
|
+
onClick: () => setShelfReloadKey(value => value + 1),
|
|
5125
|
+
}, '刷新') : null,
|
|
4770
5126
|
h('button', {
|
|
4771
5127
|
key: 'newShelf',
|
|
4772
5128
|
type: 'button',
|
|
@@ -4820,6 +5176,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4820
5176
|
}, h(Shelf, {
|
|
4821
5177
|
transport,
|
|
4822
5178
|
reloadKey: shelfReloadKey,
|
|
5179
|
+
onSummaryChange: setShelfSummary,
|
|
4823
5180
|
onOpenDoc: openFromShelf,
|
|
4824
5181
|
onDocsRemoved: onShelfDocsRemoved,
|
|
4825
5182
|
onAddDocs: (project) => {
|
|
@@ -4965,6 +5322,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
4965
5322
|
.${P}NoChange{padding:12px;border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-md,8px);
|
|
4966
5323
|
background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;margin-bottom:10px}
|
|
4967
5324
|
.${P}GenerationError{padding:8px 10px;margin-bottom:10px}
|
|
5325
|
+
.${P}GenerationOriginal{max-height:90px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.6;
|
|
5326
|
+
color:var(--dsw-alias-label-secondary);padding:6px 10px}
|
|
4968
5327
|
.${P}Processing{display:flex;align-items:center;gap:8px;padding:8px 10px;color:var(--dsw-alias-label-secondary);font-size:12px;flex:none}
|
|
4969
5328
|
.${P}WhaleTail{width:20px;height:20px;contain:strict;flex:none;display:inline-flex;position:relative;overflow:hidden}
|
|
4970
5329
|
.${P}WhaleAnimated{display:none;position:absolute;inset:0}
|
|
@@ -5174,8 +5533,18 @@ __define('./reader', (module, exports, require) => {
|
|
|
5174
5533
|
.${P}SlidePrev{left:12px}
|
|
5175
5534
|
.${P}SlideNext{right:12px}
|
|
5176
5535
|
.${P}SlideCount{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);z-index:2;
|
|
5177
|
-
color:#fff;background:rgba(0,0,0,.
|
|
5536
|
+
color:#fff;background:rgba(0,0,0,.65);border-radius:12px;padding:5px 8px;font-size:12px;max-width:calc(100% - 24px);
|
|
5178
5537
|
font-variant-numeric:tabular-nums}
|
|
5538
|
+
.${P}SlideCountControls{display:flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap}
|
|
5539
|
+
.${P}SlidePageInput{box-sizing:border-box;width:54px;min-width:0;padding:3px 4px;border:1px solid rgba(255,255,255,.4);
|
|
5540
|
+
border-radius:5px;background:rgba(255,255,255,.12);color:#fff;font:inherit;line-height:18px;text-align:center}
|
|
5541
|
+
.${P}SlidePageInput:focus-visible{outline:2px solid #fff;outline-offset:1px}
|
|
5542
|
+
.${P}SlidePageInput[aria-invalid="true"]{border-color:#ffaeae}
|
|
5543
|
+
.${P}SlideJumpButton{padding:3px 7px;border:0;border-radius:5px;background:rgba(255,255,255,.18);color:#fff;
|
|
5544
|
+
font:inherit;line-height:20px;cursor:pointer}
|
|
5545
|
+
.${P}SlideJumpButton:hover:not(:disabled){background:rgba(255,255,255,.3)}
|
|
5546
|
+
.${P}SlidePageInput:disabled,.${P}SlideJumpButton:disabled{opacity:.5;cursor:default}
|
|
5547
|
+
.${P}SlideJumpError{margin-top:4px;max-width:240px;white-space:normal;line-height:18px;color:#ffcdcd;text-align:center}
|
|
5179
5548
|
.${P}ViewBar .${P}Hint{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0}
|
|
5180
5549
|
.${P}SelectionNotice{flex:none;padding:10px 12px;font-size:13px;line-height:1.6;
|
|
5181
5550
|
color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
@@ -5245,6 +5614,12 @@ __define('./reader', (module, exports, require) => {
|
|
|
5245
5614
|
exports.bindWordLayoutLines = bindWordLayoutLines;
|
|
5246
5615
|
exports.wordNativeSelection = wordNativeSelection;
|
|
5247
5616
|
exports.wordBoxSelection = wordBoxSelection;
|
|
5617
|
+
exports.wordCompoundAnchor = wordCompoundAnchor;
|
|
5618
|
+
exports.wordAnchorText = wordAnchorText;
|
|
5619
|
+
exports.continuousWordAnchor = continuousWordAnchor;
|
|
5620
|
+
exports.wordListMarker = wordListMarker;
|
|
5621
|
+
exports.captureRequest = captureRequest;
|
|
5622
|
+
exports.groupRevisionVariants = groupRevisionVariants;
|
|
5248
5623
|
exports.rectangleSpans = rectangleSpans;
|
|
5249
5624
|
exports.placeByShapes = placeByShapes;
|
|
5250
5625
|
exports.mapOffsetToSrc = mapOffsetToSrc;
|