feat(pdf): support GraphRAG text_bbox highlighting in PDF viewer
When documents are processed through GraphRAG pipeline, coordinate enrichment produces text_bbox (paragraph-level coordinates) instead of char_positions (character-level OCR coordinates). Added resolveCharPositions() helper that converts text_bbox to CharPosition[] format, enabling PDF highlight rendering for GraphRAG-processed documents. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -93,6 +93,32 @@ export interface CharPosition {
|
||||
score: number; // OCR识别置信度
|
||||
}
|
||||
|
||||
/**
|
||||
* text_bbox -> CharPosition[] 转换
|
||||
* GraphRAG 抽取结果只有 text_bbox (段落级坐标), 没有 char_positions (字符级坐标)。
|
||||
* 将 text_bbox 转为单个 CharPosition 矩形框, 让 PdfPreview 的高亮逻辑复用。
|
||||
*/
|
||||
function resolveCharPositions(data: any): CharPosition[] | undefined {
|
||||
// 优先用 char_positions
|
||||
if (data?.char_positions && data.char_positions.length > 0) {
|
||||
return data.char_positions;
|
||||
}
|
||||
// fallback: text_bbox -> CharPosition[]
|
||||
if (data?.text_bbox) {
|
||||
const b = data.text_bbox;
|
||||
if (b.x_min != null && b.y_min != null && b.x_max != null && b.y_max != null
|
||||
&& (b.x_max - b.x_min) > 0 && (b.y_max - b.y_min) > 0) {
|
||||
return [{
|
||||
box: [[b.x_min, b.y_min], [b.x_max, b.y_min], [b.x_max, b.y_max], [b.x_min, b.y_max]],
|
||||
char: '',
|
||||
score: 1
|
||||
}];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 评查点类型定义
|
||||
* 用于展示单个评查结果
|
||||
@@ -1512,7 +1538,7 @@ export function ReviewPointsList({
|
||||
for (const item of chain) {
|
||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||
hasPage = true;
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data), item.data.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1526,7 +1552,7 @@ export function ReviewPointsList({
|
||||
// 遍历chain找到第一个有效的page
|
||||
for (const item of chain) {
|
||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data), item.data.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1566,11 +1592,11 @@ export function ReviewPointsList({
|
||||
// 假设onReviewPointSelect在作用域内可用
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions, item.data.value);
|
||||
onReviewPointSelect(reviewPointId, Number(item.data.page), resolveCharPositions(item.data), item.data.value);
|
||||
}
|
||||
}
|
||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), item.data.char_positions, item.data.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), resolveCharPositions(item.data), item.data.value);
|
||||
}
|
||||
else{
|
||||
toastService.error(`没有找到${item.field}对应的索引内容`);
|
||||
@@ -1649,11 +1675,11 @@ export function ReviewPointsList({
|
||||
if (chain[0].data.page) {
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions, chain[0].data.value);
|
||||
onReviewPointSelect(reviewPointId, chain[0].data.page, resolveCharPositions(chain[0].data), chain[0].data.value);
|
||||
}
|
||||
}
|
||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), chain[0].data.char_positions, chain[0].data.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), resolveCharPositions(chain[0].data), chain[0].data.value);
|
||||
}
|
||||
else{
|
||||
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
|
||||
@@ -1675,11 +1701,11 @@ export function ReviewPointsList({
|
||||
if (chain[1].data.page) {
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions, chain[1].data.value);
|
||||
onReviewPointSelect(reviewPointId, chain[1].data.page, resolveCharPositions(chain[1].data), chain[1].data.value);
|
||||
}
|
||||
}
|
||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), chain[1].data.char_positions, chain[1].data.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), resolveCharPositions(chain[1].data), chain[1].data.value);
|
||||
}
|
||||
else{
|
||||
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
|
||||
@@ -1815,9 +1841,9 @@ export function ReviewPointsList({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||
}
|
||||
@@ -1826,9 +1852,9 @@ export function ReviewPointsList({
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||
}
|
||||
@@ -1959,9 +1985,9 @@ export function ReviewPointsList({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), resolveCharPositions(value), value.value);
|
||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value), value.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${key}对应的索引内容`);
|
||||
}
|
||||
@@ -1971,9 +1997,9 @@ export function ReviewPointsList({
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), resolveCharPositions(value), value.value);
|
||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value), value.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${key}对应的索引内容`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user