dsh-doc-assistant 0.1.14 → 0.1.18
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 +27 -2
- package/lib/client.js +590 -174
- package/package.json +3 -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 +505 -146
- package/src/client/70-shelf.js +14 -2
- 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);
|
|
@@ -3563,6 +3795,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
3563
3795
|
const [messages, setMessages] = React.useState([]);
|
|
3564
3796
|
const [chatBusy, setChatBusy] = React.useState(false);
|
|
3565
3797
|
const [chatKind, setChatKind] = React.useState('chat');
|
|
3798
|
+
const [chatProgress, setChatProgress] = React.useState('');
|
|
3799
|
+
const chatStartedRef = React.useRef(0);
|
|
3566
3800
|
const [chatError, setChatError] = React.useState('');
|
|
3567
3801
|
const [chatModels, setChatModels] = React.useState([]);
|
|
3568
3802
|
const [chatDefaultModel, setChatDefaultModel] = React.useState(null);
|
|
@@ -3605,7 +3839,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
3605
3839
|
// Word / PPT / PDF 打开即版式;Excel 打开即表格文本,版式按需再转。
|
|
3606
3840
|
React.useEffect(() => {
|
|
3607
3841
|
const fmt = doc && doc.format;
|
|
3608
|
-
if (fmt === 'docx' || fmt === 'pptx' || fmt === 'pdf') setReaderView('layout');
|
|
3842
|
+
if (fmt === 'doc' || fmt === 'docx' || fmt === 'pptx' || fmt === 'pdf') setReaderView('layout');
|
|
3609
3843
|
else setReaderView('text');
|
|
3610
3844
|
setLayoutPage(0);
|
|
3611
3845
|
setLayoutFocus(null);
|
|
@@ -3738,7 +3972,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
3738
3972
|
uploadBusyRef.current = true;
|
|
3739
3973
|
setUploadOutcome(null);
|
|
3740
3974
|
setStatus(`正在导入 ${list.length} 个文件…`);
|
|
3741
|
-
let done = 0;
|
|
3975
|
+
let done = 0, converted = 0;
|
|
3742
3976
|
let currentFile = '';
|
|
3743
3977
|
try {
|
|
3744
3978
|
for (const file of list) {
|
|
@@ -3747,11 +3981,12 @@ __define('./reader', (module, exports, require) => {
|
|
|
3747
3981
|
setUploadProgress({ ...progress, stage: '正在读取文件…' });
|
|
3748
3982
|
const dataBase64 = await shelfModule.readFileBase64(file);
|
|
3749
3983
|
// The response also waits for best-effort cover preparation.
|
|
3750
|
-
setUploadProgress({ ...progress, stage: '正在保存文档并准备封面,请稍候…' });
|
|
3751
|
-
await transport.uploadDocs(targetProject, '', [{ name: file.name, dataBase64 }]);
|
|
3984
|
+
setUploadProgress({ ...progress, stage: /\.doc$/i.test(file.name) ? '正在转换为可修订的 Word 文档并准备封面…' : '正在保存文档并准备封面,请稍候…' });
|
|
3985
|
+
const imported = await transport.uploadDocs(targetProject, '', [{ name: file.name, dataBase64 }]);
|
|
3986
|
+
converted += (Array.isArray(imported) ? imported : imported?.entries || []).filter(entry => entry.convertedFrom === 'doc').length;
|
|
3752
3987
|
done += 1;
|
|
3753
3988
|
}
|
|
3754
|
-
const message = `已导入 ${done}
|
|
3989
|
+
const message = `已导入 ${done} 个文件${converted ? `,其中 ${converted} 个 DOC 已转换为可修订的 DOCX` : ''}`;
|
|
3755
3990
|
setStatus(message);
|
|
3756
3991
|
setUploadOutcome({ message, error: false });
|
|
3757
3992
|
} catch (error) {
|
|
@@ -3847,6 +4082,21 @@ __define('./reader', (module, exports, require) => {
|
|
|
3847
4082
|
if (draftReference && rightOpen && rightTab === 'chat' && !chatBusy && chatInputRef.current) chatInputRef.current.focus();
|
|
3848
4083
|
}, [draftReference, rightOpen, rightTab, chatBusy]);
|
|
3849
4084
|
|
|
4085
|
+
React.useEffect(() => {
|
|
4086
|
+
if (!chatBusy || !projectId || !docPath || typeof transport.contextStatus !== 'function' || !chatStartedRef.current) return undefined;
|
|
4087
|
+
let alive = true, timer;
|
|
4088
|
+
const started = chatStartedRef.current;
|
|
4089
|
+
const poll = async () => {
|
|
4090
|
+
try {
|
|
4091
|
+
const state = await transport.contextStatus(projectId, docPath);
|
|
4092
|
+
if (alive && Number(state?.at) >= started && ['reading', 'vision', 'ocr', 'ready'].includes(state.stage)) setChatProgress(state.message || '');
|
|
4093
|
+
} catch { /* The original chat request reports any real failure. */ }
|
|
4094
|
+
if (alive) timer = setTimeout(poll, 1000);
|
|
4095
|
+
};
|
|
4096
|
+
poll();
|
|
4097
|
+
return () => { alive = false; clearTimeout(timer); };
|
|
4098
|
+
}, [chatBusy, projectId, docPath, transport]);
|
|
4099
|
+
|
|
3850
4100
|
// ---- 新消息滚到底
|
|
3851
4101
|
React.useEffect(() => {
|
|
3852
4102
|
const el = chatListRef.current;
|
|
@@ -3880,6 +4130,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
3880
4130
|
setDraft('');
|
|
3881
4131
|
setDraftReference(null);
|
|
3882
4132
|
}
|
|
4133
|
+
chatStartedRef.current = Date.now();
|
|
4134
|
+
setChatProgress('');
|
|
3883
4135
|
setChatBusy(true);
|
|
3884
4136
|
setChatKind(kind);
|
|
3885
4137
|
setChatError('');
|
|
@@ -3918,6 +4170,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
3918
4170
|
if (chatInFlightRef.current && chatInFlightRef.current.target === target) return;
|
|
3919
4171
|
const request = ++chatRequestRef.current;
|
|
3920
4172
|
chatInFlightRef.current = { target, request };
|
|
4173
|
+
chatStartedRef.current = 0; setChatProgress('');
|
|
3921
4174
|
setChatBusy(true);
|
|
3922
4175
|
setChatError('');
|
|
3923
4176
|
try {
|
|
@@ -3982,57 +4235,66 @@ __define('./reader', (module, exports, require) => {
|
|
|
3982
4235
|
const payload = extra?.request ? { ...extra.request } : { projectId, path: docPath, anchor, instruction, action: actionId };
|
|
3983
4236
|
if (!extra?.request && typeof extra?.revised !== 'string') Object.assign(payload, chosenModel(chatModel));
|
|
3984
4237
|
if (!extra?.request && doc && doc.format === 'docx') {
|
|
3985
|
-
|
|
3986
|
-
payload.expectedOriginal = source.slice(anchor.srcStart, anchor.srcEnd);
|
|
4238
|
+
payload.expectedOriginal = wordAnchorText(doc, anchor);
|
|
3987
4239
|
if (doc.meta?.documentHash) payload.expectedDocumentHash = doc.meta.documentHash;
|
|
3988
4240
|
}
|
|
3989
4241
|
if (extra && typeof extra.revised === 'string') payload.revised = extra.revised;
|
|
3990
4242
|
const snapshot = captureRequest(payload);
|
|
3991
4243
|
const target = `${snapshot.projectId}\u0000${snapshot.path}`;
|
|
3992
|
-
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
4244
|
+
const range = snapshot.anchor || {};
|
|
4245
|
+
const ranges = Array.isArray(range.segments) ? range.segments : [range];
|
|
4246
|
+
const key = JSON.stringify([target, snapshot.action || actionId,
|
|
4247
|
+
snapshot.expectedDocumentHash || doc?.meta?.documentHash, snapshot.expectedOriginal ?? wordAnchorText(doc, snapshot.anchor), ranges.map(part => [part.kind,
|
|
4248
|
+
part.srcStart, part.srcEnd, part.blockId, part.sheet, part.cell, part.row, part.col])]);
|
|
4249
|
+
if (Array.from(generationJobsRef.current.values()).some(job => job.key === key && job.state === 'running')) return;
|
|
4250
|
+
const id = extra?.jobId || `generation-${++generationSequenceRef.current}`;
|
|
4251
|
+
const operation = Object.freeze({ id, key, target, snapshot, state: 'running', replaceId: extra?.replaceId,
|
|
4252
|
+
original: snapshot.expectedOriginal ?? wordAnchorText(doc, snapshot.anchor),
|
|
4253
|
+
documentHash: snapshot.expectedDocumentHash || doc?.meta?.documentHash,
|
|
4254
|
+
label: actionId === 'expand' ? '扩写' : actionId === 'manual' ? '修改' : '润色' });
|
|
4255
|
+
updateGenerationJobs(previous => { const next = new Map(previous); next.set(id, operation); return next; });
|
|
4256
|
+
const isCurrent = () => generationAliveRef.current && generationTargetRef.current === target
|
|
4257
|
+
&& store.projectId === snapshot.projectId && store.docPath === snapshot.path;
|
|
4258
|
+
const finishJob = changes => updateGenerationJobs(previous => {
|
|
4259
|
+
if (previous.get(id) !== operation) return previous;
|
|
4260
|
+
const next = new Map(previous);
|
|
4261
|
+
if (changes) next.set(id, Object.freeze({ ...operation, ...changes }));
|
|
4262
|
+
else next.delete(id);
|
|
4263
|
+
return next;
|
|
4264
|
+
});
|
|
3996
4265
|
let replaced = false;
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
// 生成后让用户直接看到结果:展开右栏并切到「修订」标签。
|
|
4003
|
-
setRightOpen(true);
|
|
4004
|
-
setRightTab('revise');
|
|
4266
|
+
if (isCurrent()) {
|
|
4267
|
+
setStatus(extra?.replaceId ? '正在重新生成…' : '正在生成修订…');
|
|
4268
|
+
setSelectionNotice(''); setNoChangeNotice('');
|
|
4269
|
+
setRightOpen(true); setRightTab('revise');
|
|
4270
|
+
}
|
|
4005
4271
|
try {
|
|
4006
4272
|
if (extra?.replaceId) {
|
|
4007
4273
|
await store.delete(extra.replaceId, { optimistic: true });
|
|
4008
4274
|
replaced = true;
|
|
4009
4275
|
}
|
|
4010
4276
|
const revision = await store.generate(snapshot);
|
|
4011
|
-
if (!isCurrent()) return revision;
|
|
4012
4277
|
if (revision?.unchanged) {
|
|
4013
|
-
const message = `${
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
setStatus(message);
|
|
4017
|
-
}
|
|
4278
|
+
const message = `${operation.label}结果与原文一致,内容无变化。`;
|
|
4279
|
+
finishJob({ state: 'unchanged', message, replaceId: undefined });
|
|
4280
|
+
if (isCurrent() && !extra?.batch) setStatus(message);
|
|
4018
4281
|
return revision;
|
|
4019
4282
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4283
|
+
finishJob(null);
|
|
4284
|
+
if (isCurrent()) {
|
|
4285
|
+
const note = extra && extra.note ? `${extra.note} ` : '';
|
|
4286
|
+
setStatus(`${note}${revision && revision.id ? `已生成修订 ${revision.id}` : '已生成修订'}`);
|
|
4287
|
+
}
|
|
4022
4288
|
return revision;
|
|
4023
4289
|
} 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;
|
|
4290
|
+
const message = `${extra?.replaceId ? '重新生成' : operation.label}失败:${normalizeError(error)}`;
|
|
4291
|
+
finishJob({ state: 'failed', message, replaceId: !replaced ? extra?.replaceId : undefined });
|
|
4292
|
+
if (isCurrent()) setStatus(message);
|
|
4031
4293
|
}
|
|
4032
|
-
}, [store, projectId, docPath, doc, chatModel]);
|
|
4294
|
+
}, [store, projectId, docPath, doc, chatModel, updateGenerationJobs]);
|
|
4033
4295
|
|
|
4034
4296
|
const refreshDoc = React.useCallback(() => {
|
|
4035
|
-
if (docPath) openDoc(docPath, { keepStatus: true });
|
|
4297
|
+
if (docPath) return openDoc(docPath, { keepStatus: true });
|
|
4036
4298
|
}, [docPath, openDoc]);
|
|
4037
4299
|
|
|
4038
4300
|
const reloadDocument = async (automatic = false) => {
|
|
@@ -4081,83 +4343,86 @@ __define('./reader', (module, exports, require) => {
|
|
|
4081
4343
|
};
|
|
4082
4344
|
|
|
4083
4345
|
const acceptRevision = React.useCallback(async (revision) => {
|
|
4084
|
-
if (!revision) return;
|
|
4085
|
-
|
|
4346
|
+
if (!revision || writeBusyRef.current) return;
|
|
4347
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4348
|
+
const isCurrent = () => generationTargetRef.current === target && `${store.projectId}\u0000${store.docPath}` === target;
|
|
4349
|
+
writeBusyRef.current = true; setBusyAction(`rev:${revision.id}`); setAcceptingRevisionId(revision.id);
|
|
4350
|
+
setStatus(/\.docx$/i.test(revision.docPath || '') ? '正在核对版式并写回…' : '正在写回修订…');
|
|
4351
|
+
setRevisionErrors(previous => { const next = new Map(previous); next.delete(revision.id); return next; });
|
|
4086
4352
|
try {
|
|
4087
|
-
const out = await store.accept(revision.id
|
|
4353
|
+
const out = await store.accept(revision.id, revision.layoutWarning?.token
|
|
4354
|
+
? { layoutRiskToken: revision.layoutWarning.token } : undefined);
|
|
4355
|
+
if (!isCurrent()) return;
|
|
4088
4356
|
const written = out && out.written;
|
|
4089
|
-
setStatus(written
|
|
4090
|
-
?
|
|
4091
|
-
: '已接受修订');
|
|
4092
|
-
setPreviewEpoch(
|
|
4093
|
-
refreshDoc();
|
|
4357
|
+
setStatus(written?.layoutRiskAccepted
|
|
4358
|
+
? '已接受并写入修订,请点击文档标题右侧的 Office 图标,手动打开文档调整排版。'
|
|
4359
|
+
: written && written.backup ? `已接受并写回(${written.bytes || 0} 字节,备份:${written.backup})` : '已接受修订');
|
|
4360
|
+
setPreviewEpoch(value => value + 1);
|
|
4361
|
+
await Promise.all([refreshDoc(), store.load()]);
|
|
4094
4362
|
} catch (error) {
|
|
4095
|
-
|
|
4363
|
+
if (!isCurrent()) return;
|
|
4364
|
+
const message = normalizeError(error);
|
|
4365
|
+
setStatus(`${error?.body?.revision?.layoutWarning ? '排版提示' : '接受失败'}:${message}`);
|
|
4366
|
+
setRevisionErrors(previous => { const next = new Map(previous); next.set(revision.id, message); return next; });
|
|
4096
4367
|
} finally {
|
|
4097
|
-
setBusyAction('');
|
|
4368
|
+
writeBusyRef.current = false; setBusyAction(''); setAcceptingRevisionId('');
|
|
4098
4369
|
}
|
|
4099
4370
|
}, [store, refreshDoc]);
|
|
4100
4371
|
|
|
4101
4372
|
const rejectRevision = React.useCallback(async (revision) => {
|
|
4102
4373
|
if (!revision) return;
|
|
4103
|
-
|
|
4374
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4104
4375
|
try {
|
|
4105
4376
|
await store.reject(revision.id);
|
|
4106
|
-
setStatus('已撤回修订');
|
|
4377
|
+
if (generationTargetRef.current === target) setStatus('已撤回修订');
|
|
4107
4378
|
} catch (error) {
|
|
4108
|
-
setStatus(`撤回失败:${normalizeError(error)}`);
|
|
4109
|
-
} finally {
|
|
4110
|
-
setBusyAction('');
|
|
4379
|
+
if (generationTargetRef.current === target) setStatus(`撤回失败:${normalizeError(error)}`);
|
|
4111
4380
|
}
|
|
4112
4381
|
}, [store]);
|
|
4113
4382
|
|
|
4114
4383
|
const deleteRevision = React.useCallback(async (revision) => {
|
|
4115
4384
|
if (!revision) return;
|
|
4116
|
-
|
|
4385
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4117
4386
|
try {
|
|
4118
|
-
await store.delete(revision.id);
|
|
4119
|
-
setStatus(revision.status === 'accepted'
|
|
4387
|
+
await store.delete(revision.id, { optimistic: true });
|
|
4388
|
+
if (generationTargetRef.current === target) setStatus(revision.status === 'accepted'
|
|
4389
|
+
? '已删除修订卡片,文档中已写入的修改保留。' : '已删除修订卡片');
|
|
4120
4390
|
} catch (error) {
|
|
4121
|
-
setStatus(`删除修订失败:${normalizeError(error)}`);
|
|
4122
|
-
}
|
|
4391
|
+
if (generationTargetRef.current === target) setStatus(`删除修订失败:${normalizeError(error)}`);
|
|
4392
|
+
}
|
|
4123
4393
|
}, [store]);
|
|
4124
4394
|
|
|
4125
4395
|
const restoreRevision = React.useCallback(async (revision) => {
|
|
4126
|
-
if (!revision) return;
|
|
4127
|
-
|
|
4396
|
+
if (!revision || writeBusyRef.current) return;
|
|
4397
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4398
|
+
writeBusyRef.current = true; setBusyAction(`rev:${revision.id}`);
|
|
4128
4399
|
try {
|
|
4129
4400
|
await store.restore(revision.id);
|
|
4401
|
+
if (generationTargetRef.current !== target) return;
|
|
4130
4402
|
setStatus(revision.status === 'accepted' ? '已撤销写入并还原文件' : '已恢复为待处理');
|
|
4131
|
-
setPreviewEpoch(
|
|
4132
|
-
refreshDoc();
|
|
4133
|
-
await store.load();
|
|
4403
|
+
setPreviewEpoch(value => value + 1);
|
|
4404
|
+
await Promise.all([refreshDoc(), store.load()]);
|
|
4134
4405
|
} catch (error) {
|
|
4135
|
-
setStatus(`恢复失败:${normalizeError(error)}`);
|
|
4136
|
-
} finally {
|
|
4137
|
-
setBusyAction('');
|
|
4138
|
-
}
|
|
4406
|
+
if (generationTargetRef.current === target) setStatus(`恢复失败:${normalizeError(error)}`);
|
|
4407
|
+
} finally { writeBusyRef.current = false; setBusyAction(''); }
|
|
4139
4408
|
}, [store, refreshDoc]);
|
|
4140
4409
|
|
|
4141
4410
|
const restoreWriteGroup = React.useCallback(async (group) => {
|
|
4142
|
-
const list = Array.isArray(group) ? group.filter(
|
|
4143
|
-
if (list.length
|
|
4144
|
-
|
|
4411
|
+
const list = Array.isArray(group) ? group.filter(item => item && item.id) : [];
|
|
4412
|
+
if (!list.length || writeBusyRef.current) return;
|
|
4413
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4414
|
+
writeBusyRef.current = true; setBusyAction(`rev:${list[0].id}`);
|
|
4145
4415
|
try {
|
|
4146
|
-
const results = await store.bulk(list.map(
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
return;
|
|
4151
|
-
}
|
|
4416
|
+
const results = await store.bulk(list.map(item => item.id), 'restore');
|
|
4417
|
+
if (generationTargetRef.current !== target) return;
|
|
4418
|
+
const failed = (results || []).filter(item => item && item.ok === false);
|
|
4419
|
+
if (failed.length) { setStatus(`整组撤回失败:${failed[0].error || ''}`); return; }
|
|
4152
4420
|
setStatus(`已撤回本次批量写入(${list.length} 条)`);
|
|
4153
|
-
setPreviewEpoch(
|
|
4154
|
-
refreshDoc();
|
|
4155
|
-
await store.load();
|
|
4421
|
+
setPreviewEpoch(value => value + 1);
|
|
4422
|
+
await Promise.all([refreshDoc(), store.load()]);
|
|
4156
4423
|
} catch (error) {
|
|
4157
|
-
setStatus(`整组撤回失败:${normalizeError(error)}`);
|
|
4158
|
-
} finally {
|
|
4159
|
-
setBusyAction('');
|
|
4160
|
-
}
|
|
4424
|
+
if (generationTargetRef.current === target) setStatus(`整组撤回失败:${normalizeError(error)}`);
|
|
4425
|
+
} finally { writeBusyRef.current = false; setBusyAction(''); }
|
|
4161
4426
|
}, [store, refreshDoc]);
|
|
4162
4427
|
|
|
4163
4428
|
const regenerate = React.useCallback(async (revision) => {
|
|
@@ -4176,15 +4441,21 @@ __define('./reader', (module, exports, require) => {
|
|
|
4176
4441
|
}, [generate, projectId, docPath, chatModel]);
|
|
4177
4442
|
|
|
4178
4443
|
const acceptAll = React.useCallback(async () => {
|
|
4444
|
+
if (writeBusyRef.current) return;
|
|
4179
4445
|
if (groupRevisionVariants(store.all()).some(group => group.items.filter(item => item.status === 'pending').length > 1)) {
|
|
4180
4446
|
setStatus('同一选区有多份扩写,请选择其中一份接受。');
|
|
4181
4447
|
return;
|
|
4182
4448
|
}
|
|
4449
|
+
const target = `${store.projectId}\u0000${store.docPath}`;
|
|
4450
|
+
writeBusyRef.current = true;
|
|
4183
4451
|
setBusyAction('accept-all');
|
|
4184
4452
|
setStatus('正在批量接受…');
|
|
4185
4453
|
try {
|
|
4186
4454
|
const results = await store.acceptAll();
|
|
4455
|
+
if (generationTargetRef.current !== target) return;
|
|
4187
4456
|
const failed = (results || []).filter((item) => item && item.ok === false);
|
|
4457
|
+
if (failed.length) setRevisionErrors(previous => { const next = new Map(previous);
|
|
4458
|
+
failed.forEach(item => next.set(item.revisionId, item.error || '接受失败')); return next; });
|
|
4188
4459
|
const wrote = (results || []).some((item) => item && item.ok !== false);
|
|
4189
4460
|
setStatus(failed.length > 0
|
|
4190
4461
|
? `接受完成,${failed.length} 条失败:${failed[0].error || ''}`
|
|
@@ -4192,9 +4463,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
4192
4463
|
if (wrote) setPreviewEpoch((value) => value + 1);
|
|
4193
4464
|
refreshDoc();
|
|
4194
4465
|
} catch (error) {
|
|
4195
|
-
setStatus(`批量接受失败:${normalizeError(error)}`);
|
|
4466
|
+
if (generationTargetRef.current === target) setStatus(`批量接受失败:${normalizeError(error)}`);
|
|
4196
4467
|
} finally {
|
|
4197
|
-
setBusyAction('');
|
|
4468
|
+
writeBusyRef.current = false; setBusyAction('');
|
|
4198
4469
|
}
|
|
4199
4470
|
}, [store, refreshDoc]);
|
|
4200
4471
|
|
|
@@ -4262,18 +4533,48 @@ __define('./reader', (module, exports, require) => {
|
|
|
4262
4533
|
if (!instruction) return;
|
|
4263
4534
|
// 只读格式即使工具条漏了按钮,也不再打出「改写失败」。
|
|
4264
4535
|
if (!revisionAllowed(doc)) return;
|
|
4536
|
+
const isWordEdit = doc?.format === 'docx' && (actionId === 'polish' || actionId === 'expand');
|
|
4537
|
+
if (isWordEdit && wordListMarker(hit.text)) {
|
|
4538
|
+
setNoChangeNotice('选区只有列表标记,没有需要修改的正文。');
|
|
4539
|
+
setStatus('已保留原列表标记,内容无变化。');
|
|
4540
|
+
return;
|
|
4541
|
+
}
|
|
4265
4542
|
const anchor = hit.anchor || {};
|
|
4266
4543
|
const hasRange = Number.isInteger(anchor.srcStart) && Number.isInteger(anchor.srcEnd);
|
|
4267
4544
|
const parts = Array.isArray(hit.parts) ? hit.parts : [];
|
|
4268
4545
|
const shapeNote = hit.matched === 'shape' ? '识别和原文不一致,已按整个文本框改写。' : '';
|
|
4546
|
+
if (isWordEdit && (parts.length > 1 || Array.isArray(anchor.segments))) {
|
|
4547
|
+
const allParts = parts.length ? parts : (anchor.segments || []).map(segment => ({ anchor: segment,
|
|
4548
|
+
text: wordAnchorText(doc, segment) }));
|
|
4549
|
+
if (!wordCompoundAnchor(doc, allParts)) {
|
|
4550
|
+
cannotLocate(); return;
|
|
4551
|
+
}
|
|
4552
|
+
let firstBody = 0;
|
|
4553
|
+
while (firstBody < allParts.length && wordListMarker(allParts[firstBody].text)) firstBody += 1;
|
|
4554
|
+
const body = allParts.slice(firstBody), combined = wordCompoundAnchor(doc, body);
|
|
4555
|
+
if (!combined || body.some(part => wordListMarker(part.text)) || !continuousWordAnchor(doc, combined)) {
|
|
4556
|
+
const message = '选区中间有未选中的文字或列表标记,请连续选择一段正文后再修改。';
|
|
4557
|
+
setSelectionNotice(message); setStatus(message); return;
|
|
4558
|
+
}
|
|
4559
|
+
// One model request sees the complete selected paragraph. Per-line
|
|
4560
|
+
// rewriting could skip an unchanged first line and fragment the result.
|
|
4561
|
+
generate(combined, instruction, actionId);
|
|
4562
|
+
return;
|
|
4563
|
+
}
|
|
4269
4564
|
if (!hasRange && anchor.kind !== 'cell' && parts.length > 1) {
|
|
4270
|
-
const
|
|
4565
|
+
const located = parts.filter((part) => (
|
|
4271
4566
|
part && part.anchor && Number.isInteger(part.anchor.srcStart) && Number.isInteger(part.anchor.srcEnd)
|
|
4272
4567
|
));
|
|
4273
|
-
if (
|
|
4568
|
+
if (located.length === 0 || (doc && doc.format === 'docx' && located.length < parts.length)) {
|
|
4274
4569
|
cannotLocate();
|
|
4275
4570
|
return;
|
|
4276
4571
|
}
|
|
4572
|
+
const ready = located;
|
|
4573
|
+
if (!ready.length) {
|
|
4574
|
+
setNoChangeNotice('选区只有列表标记,没有需要修改的正文。');
|
|
4575
|
+
setStatus('已保留原列表标记,内容无变化。');
|
|
4576
|
+
return;
|
|
4577
|
+
}
|
|
4277
4578
|
(async () => {
|
|
4278
4579
|
let unchanged = 0, changed = 0, failed = 0;
|
|
4279
4580
|
for (let i = 0; i < ready.length; i += 1) {
|
|
@@ -4287,7 +4588,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4287
4588
|
if (unchanged) setNoChangeNotice(unchanged === ready.length
|
|
4288
4589
|
? '所选内容与原文一致,内容无变化。' : `${unchanged} 段内容无变化,已跳过。`);
|
|
4289
4590
|
setStatus([changed ? `已生成 ${changed} 段修订` : '', unchanged ? `${unchanged} 段内容无变化` : '',
|
|
4290
|
-
failed ? `${failed} 段处理失败` : '',
|
|
4591
|
+
failed ? `${failed} 段处理失败` : '', located.length < parts.length ? `${parts.length - located.length} 段对不上原文` : ''].filter(Boolean).join(';'));
|
|
4291
4592
|
})();
|
|
4292
4593
|
return;
|
|
4293
4594
|
}
|
|
@@ -4423,6 +4724,28 @@ __define('./reader', (module, exports, require) => {
|
|
|
4423
4724
|
: treeNodes('', 0, treeCtx)),
|
|
4424
4725
|
]);
|
|
4425
4726
|
|
|
4727
|
+
const convertLegacyWord = async () => {
|
|
4728
|
+
if (legacyConvertRef.current || docBusy || busyAction || !projectId || !docPath) return;
|
|
4729
|
+
const target = `${projectId}\u0000${docPath}`, operation = { target };
|
|
4730
|
+
legacyConvertRef.current = operation; setLegacyConverting(true); setOfficeNotice(null);
|
|
4731
|
+
setStatus('正在转换为可修订的 Word 文档…');
|
|
4732
|
+
try {
|
|
4733
|
+
const entry = await transport.convertWord(projectId, docPath);
|
|
4734
|
+
if (!entry?.relPath) throw new Error('转换未返回可编辑的 Word 文件');
|
|
4735
|
+
setShelfReloadKey(value => value + 1);
|
|
4736
|
+
if (chatTargetRef.current === target) {
|
|
4737
|
+
await openDoc(entry.relPath, { projectId });
|
|
4738
|
+
setStatus(`已打开可修订副本 ${entry.name || entry.relPath}`);
|
|
4739
|
+
}
|
|
4740
|
+
} catch (error) {
|
|
4741
|
+
if (chatTargetRef.current === target) {
|
|
4742
|
+
setOfficeNotice({ message: normalizeError(error) }); setStatus(`转换失败:${normalizeError(error)}`);
|
|
4743
|
+
}
|
|
4744
|
+
} finally {
|
|
4745
|
+
if (legacyConvertRef.current === operation) { legacyConvertRef.current = null; setLegacyConverting(false); }
|
|
4746
|
+
}
|
|
4747
|
+
};
|
|
4748
|
+
|
|
4426
4749
|
// ---- 渲染:中栏正文
|
|
4427
4750
|
const center = h(DocReader, {
|
|
4428
4751
|
key: 'center',
|
|
@@ -4430,6 +4753,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4430
4753
|
docBusy,
|
|
4431
4754
|
docError,
|
|
4432
4755
|
pendingByBlock,
|
|
4756
|
+
visionRoute: chosenModel(chatModel),
|
|
4433
4757
|
busyAction: busyAction || (chatBusy && chatKind === 'translate' ? 'translate' : ''),
|
|
4434
4758
|
actions: SELECTION_ACTIONS,
|
|
4435
4759
|
projectId,
|
|
@@ -4449,6 +4773,9 @@ __define('./reader', (module, exports, require) => {
|
|
|
4449
4773
|
onAction: runAction,
|
|
4450
4774
|
previewEpoch,
|
|
4451
4775
|
officeNotice,
|
|
4776
|
+
onConvertWord: convertLegacyWord,
|
|
4777
|
+
convertingWord: legacyConverting,
|
|
4778
|
+
conversionDisabled: docBusy || Boolean(busyAction),
|
|
4452
4779
|
onRefresh: reloadDocument,
|
|
4453
4780
|
refreshDisabled: docBusy || Boolean(busyAction),
|
|
4454
4781
|
onAccept: acceptRevision,
|
|
@@ -4528,7 +4855,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4528
4855
|
messageFooter(message),
|
|
4529
4856
|
])),
|
|
4530
4857
|
chatBusy ? h('div', { key: 'thinking', className: P + 'Thinking', 'data-chat-thinking': '1' },
|
|
4531
|
-
h(Processing, { kind: chatKind, text: chatKind === 'translate' ? '正在翻译…' : '思考中…' })) : null,
|
|
4858
|
+
h(Processing, { kind: chatKind, text: chatProgress || (chatKind === 'translate' ? '正在翻译…' : '思考中…') })) : null,
|
|
4532
4859
|
),
|
|
4533
4860
|
chatError ? h('div', { key: 'chatErr', className: P + 'Err', 'data-chat-error': '1' }, chatError) : null,
|
|
4534
4861
|
h('div', { key: 'composer', className: P + 'Composer' }, [
|
|
@@ -4590,7 +4917,7 @@ __define('./reader', (module, exports, require) => {
|
|
|
4590
4917
|
h('option', { key: 'default', value: '' }, chatModelsBusy ? '加载模型…' : `跟随 DSH${chatDefaultModel ? ` · ${chatDefaultModel.model}` : ''}`),
|
|
4591
4918
|
chatModel && !chatModels.some(item => JSON.stringify([item.provider, item.model]) === chatModel)
|
|
4592
4919
|
? 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}`)),
|
|
4920
|
+
...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
4921
|
]),
|
|
4595
4922
|
]),
|
|
4596
4923
|
h('div', { key: 'hint', className: P + 'Hint' },
|
|
@@ -4606,7 +4933,11 @@ __define('./reader', (module, exports, require) => {
|
|
|
4606
4933
|
collapsed: collapsedRevisions.includes(item.id),
|
|
4607
4934
|
onToggleCollapsed: id => setCollapsedRevisions(previous => previous.includes(id)
|
|
4608
4935
|
? previous.filter(value => value !== id) : previous.concat(id).slice(-500)),
|
|
4609
|
-
busy:
|
|
4936
|
+
busy: store.isBusy(`rev:${item.id}`),
|
|
4937
|
+
writeBusy: Boolean(busyAction),
|
|
4938
|
+
acceptError: revisionErrors.get(item.id),
|
|
4939
|
+
progress: acceptingRevisionId === item.id ? h(Processing, { kind: 'accept',
|
|
4940
|
+
text: /\.docx$/i.test(item.docPath || '') ? '正在核对版式并写回…' : '正在写回修订…' }) : null,
|
|
4610
4941
|
writeGroup: (revision) => store.writeGroup(revision),
|
|
4611
4942
|
onAccept: acceptRevision,
|
|
4612
4943
|
onReject: rejectRevision,
|
|
@@ -4624,16 +4955,26 @@ __define('./reader', (module, exports, require) => {
|
|
|
4624
4955
|
store.loading ? h('div', { className: P + 'Hint' }, '加载修订中…') : null,
|
|
4625
4956
|
store.error ? h('div', { className: P + 'Err' }, store.error) : null,
|
|
4626
4957
|
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
|
-
|
|
4958
|
+
Array.from(generationJobs.values()).filter(job => job.target === generationTargetRef.current).reverse().map(job =>
|
|
4959
|
+
h('div', { key: job.id, className: P + 'RevCard', 'data-generation-id': job.id,
|
|
4960
|
+
'data-generation-state': job.state, 'data-generation-action': job.snapshot.action,
|
|
4961
|
+
'data-generation-error': job.state === 'failed' ? '1' : undefined,
|
|
4962
|
+
role: job.state === 'failed' ? 'alert' : 'status' }, [
|
|
4963
|
+
h('div', { key: 'label', className: P + 'RevCollapse' }, `${job.label} · ${anchorLabel(job.snapshot.anchor)}`),
|
|
4964
|
+
typeof job.original === 'string' ? h('div', { key: 'original', className: P + 'GenerationOriginal',
|
|
4965
|
+
title: job.original }, job.original) : null,
|
|
4966
|
+
job.state === 'running' ? h(Processing, { key: 'progress', kind: job.snapshot.action,
|
|
4967
|
+
text: `正在${job.label}…` }) : null,
|
|
4968
|
+
job.message ? h('div', { key: 'message', className: job.state === 'failed' ? P + 'Err' : P + 'NoChange',
|
|
4969
|
+
'data-revision-unchanged': job.state === 'unchanged' ? '1' : undefined }, job.message) : null,
|
|
4970
|
+
job.state === 'failed' ? h('button', { key: 'retry', type: 'button', className: P + 'Btn',
|
|
4971
|
+
onClick: () => generate(job.snapshot.anchor, job.snapshot.instruction, job.snapshot.action,
|
|
4972
|
+
{ request: job.snapshot, replaceId: job.replaceId, jobId: job.id }) }, '重试') : null,
|
|
4973
|
+
job.state !== 'running' ? h('button', { key: 'dismiss', type: 'button', className: P + 'Btn',
|
|
4974
|
+
onClick: () => updateGenerationJobs(previous => { const next = new Map(previous); next.delete(job.id); return next; }) }, '删除') : null,
|
|
4975
|
+
])),
|
|
4976
|
+
items.length === 0 && !store.loading && !noChangeNotice
|
|
4977
|
+
&& !Array.from(generationJobs.values()).some(job => job.target === generationTargetRef.current)
|
|
4637
4978
|
? h('div', { className: P + 'Hint' }, revisionAllowed(doc)
|
|
4638
4979
|
? '选中正文后点「润色 / 扩写」生成修订;接受才会写回文件。'
|
|
4639
4980
|
: '这份文档只能阅读,不能生成写回修订。')
|
|
@@ -4965,6 +5306,8 @@ __define('./reader', (module, exports, require) => {
|
|
|
4965
5306
|
.${P}NoChange{padding:12px;border:1px solid var(--dsw-alias-border-l1);border-radius:var(--dsw-radius-md,8px);
|
|
4966
5307
|
background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px;margin-bottom:10px}
|
|
4967
5308
|
.${P}GenerationError{padding:8px 10px;margin-bottom:10px}
|
|
5309
|
+
.${P}GenerationOriginal{max-height:90px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.6;
|
|
5310
|
+
color:var(--dsw-alias-label-secondary);padding:6px 10px}
|
|
4968
5311
|
.${P}Processing{display:flex;align-items:center;gap:8px;padding:8px 10px;color:var(--dsw-alias-label-secondary);font-size:12px;flex:none}
|
|
4969
5312
|
.${P}WhaleTail{width:20px;height:20px;contain:strict;flex:none;display:inline-flex;position:relative;overflow:hidden}
|
|
4970
5313
|
.${P}WhaleAnimated{display:none;position:absolute;inset:0}
|
|
@@ -5174,8 +5517,18 @@ __define('./reader', (module, exports, require) => {
|
|
|
5174
5517
|
.${P}SlidePrev{left:12px}
|
|
5175
5518
|
.${P}SlideNext{right:12px}
|
|
5176
5519
|
.${P}SlideCount{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);z-index:2;
|
|
5177
|
-
color:#fff;background:rgba(0,0,0,.
|
|
5520
|
+
color:#fff;background:rgba(0,0,0,.65);border-radius:12px;padding:5px 8px;font-size:12px;max-width:calc(100% - 24px);
|
|
5178
5521
|
font-variant-numeric:tabular-nums}
|
|
5522
|
+
.${P}SlideCountControls{display:flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap}
|
|
5523
|
+
.${P}SlidePageInput{box-sizing:border-box;width:54px;min-width:0;padding:3px 4px;border:1px solid rgba(255,255,255,.4);
|
|
5524
|
+
border-radius:5px;background:rgba(255,255,255,.12);color:#fff;font:inherit;line-height:18px;text-align:center}
|
|
5525
|
+
.${P}SlidePageInput:focus-visible{outline:2px solid #fff;outline-offset:1px}
|
|
5526
|
+
.${P}SlidePageInput[aria-invalid="true"]{border-color:#ffaeae}
|
|
5527
|
+
.${P}SlideJumpButton{padding:3px 7px;border:0;border-radius:5px;background:rgba(255,255,255,.18);color:#fff;
|
|
5528
|
+
font:inherit;line-height:20px;cursor:pointer}
|
|
5529
|
+
.${P}SlideJumpButton:hover:not(:disabled){background:rgba(255,255,255,.3)}
|
|
5530
|
+
.${P}SlidePageInput:disabled,.${P}SlideJumpButton:disabled{opacity:.5;cursor:default}
|
|
5531
|
+
.${P}SlideJumpError{margin-top:4px;max-width:240px;white-space:normal;line-height:18px;color:#ffcdcd;text-align:center}
|
|
5179
5532
|
.${P}ViewBar .${P}Hint{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0}
|
|
5180
5533
|
.${P}SelectionNotice{flex:none;padding:10px 12px;font-size:13px;line-height:1.6;
|
|
5181
5534
|
color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border-bottom:1px solid var(--dsw-alias-border-l1)}
|
|
@@ -5245,6 +5598,12 @@ __define('./reader', (module, exports, require) => {
|
|
|
5245
5598
|
exports.bindWordLayoutLines = bindWordLayoutLines;
|
|
5246
5599
|
exports.wordNativeSelection = wordNativeSelection;
|
|
5247
5600
|
exports.wordBoxSelection = wordBoxSelection;
|
|
5601
|
+
exports.wordCompoundAnchor = wordCompoundAnchor;
|
|
5602
|
+
exports.wordAnchorText = wordAnchorText;
|
|
5603
|
+
exports.continuousWordAnchor = continuousWordAnchor;
|
|
5604
|
+
exports.wordListMarker = wordListMarker;
|
|
5605
|
+
exports.captureRequest = captureRequest;
|
|
5606
|
+
exports.groupRevisionVariants = groupRevisionVariants;
|
|
5248
5607
|
exports.rectangleSpans = rectangleSpans;
|
|
5249
5608
|
exports.placeByShapes = placeByShapes;
|
|
5250
5609
|
exports.mapOffsetToSrc = mapOffsetToSrc;
|