Skip to content

Commit 7f0b7e2

Browse files
committed
feat: Add https://api.pageindex.ai to CSP connect-src` and blur the active element on document submission.
1 parent ed5a247 commit 7f0b7e2

File tree

4 files changed

+272
-23
lines changed

4 files changed

+272
-23
lines changed

src/components/DashboardViews/DocumentsView.tsx

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,38 @@ function getFileIcon(fileType: string) {
5858
}
5959
}
6060

61-
function getMaxPageIndex(nodes: PageIndexNode[]): number {
62-
let maxPage = 0;
63-
for (const node of nodes) {
64-
if (typeof node.page_index === 'number') {
65-
maxPage = Math.max(maxPage, node.page_index);
66-
} else if (typeof node.page_number === 'number') {
67-
maxPage = Math.max(maxPage, node.page_number);
61+
function getPageCount(nodes: PageIndexNode[]): number | null {
62+
let minPage = Number.POSITIVE_INFINITY;
63+
let maxPage = Number.NEGATIVE_INFINITY;
64+
let found = false;
65+
66+
const walk = (node: PageIndexNode) => {
67+
const pageValue = typeof node.page_index === 'number'
68+
? node.page_index
69+
: typeof node.page_number === 'number'
70+
? node.page_number
71+
: null;
72+
73+
if (pageValue !== null) {
74+
found = true;
75+
minPage = Math.min(minPage, pageValue);
76+
maxPage = Math.max(maxPage, pageValue);
6877
}
78+
6979
if (node.nodes && node.nodes.length > 0) {
70-
maxPage = Math.max(maxPage, getMaxPageIndex(node.nodes));
80+
node.nodes.forEach(walk);
7181
}
82+
};
83+
84+
nodes.forEach(walk);
85+
86+
if (!found) return null;
87+
88+
// If pages are 0-indexed, convert to count
89+
if (minPage === 0) {
90+
return maxPage + 1;
7291
}
92+
7393
return maxPage;
7494
}
7595

@@ -173,6 +193,8 @@ export default function DocumentsView({ isDark = true }: { isDark?: boolean }) {
173193
const [pageIndexTree, setPageIndexTree] = useState<PageIndexNode[]>([]);
174194
const [pageIndexByDocId, setPageIndexByDocId] = useState<Record<string, PageIndexNode[]>>({});
175195
const [pageIndexDocIdByDocId, setPageIndexDocIdByDocId] = useState<Record<string, string>>({});
196+
const [pageIndexLoadingByDocId, setPageIndexLoadingByDocId] = useState<Record<string, boolean>>({});
197+
const [pageIndexErrorByDocId, setPageIndexErrorByDocId] = useState<Record<string, string>>({});
176198
const [chatMessages, setChatMessages] = useState<{ role: string; content: string }[]>([]);
177199
const [chatInput, setChatInput] = useState('');
178200
const [isChatting, setIsChatting] = useState(false);
@@ -300,8 +322,9 @@ export default function DocumentsView({ isDark = true }: { isDark?: boolean }) {
300322
kbId,
301323
user.id,
302324
() => { } // Silent progress for standard RAG
303-
).then((ragDoc) => {
325+
).then(async (ragDoc) => {
304326
if (ragDoc) {
327+
await ragService.setPageIndexDocId(ragDoc.id, docId);
305328
setPageIndexByDocId(prev => ({ ...prev, [ragDoc.id]: result }));
306329
setPageIndexDocIdByDocId(prev => ({ ...prev, [ragDoc.id]: docId }));
307330
}
@@ -349,12 +372,41 @@ export default function DocumentsView({ isDark = true }: { isDark?: boolean }) {
349372
}, [chatMessages, isChatting]);
350373

351374
// ─── Expand / view structure ──────────────────────────────
352-
const toggleExpand = (docId: string) => {
375+
const resolvePageIndexDocId = (doc: UploadedDocument) =>
376+
doc.pageindex_doc_id || pageIndexDocIdByDocId[doc.id];
377+
378+
const fetchPageIndexTreeForDoc = async (doc: UploadedDocument) => {
379+
const pageIndexId = resolvePageIndexDocId(doc);
380+
if (!pageIndexId || pageIndexByDocId[doc.id] || pageIndexLoadingByDocId[doc.id]) return;
381+
382+
setPageIndexLoadingByDocId(prev => ({ ...prev, [doc.id]: true }));
383+
setPageIndexErrorByDocId(prev => {
384+
const next = { ...prev };
385+
delete next[doc.id];
386+
return next;
387+
});
388+
389+
try {
390+
const tree = await pageIndexService.getTree(pageIndexId);
391+
setPageIndexByDocId(prev => ({ ...prev, [doc.id]: tree }));
392+
} catch (err: any) {
393+
setPageIndexErrorByDocId(prev => ({
394+
...prev,
395+
[doc.id]: err?.message || 'Failed to load PageIndex structure',
396+
}));
397+
} finally {
398+
setPageIndexLoadingByDocId(prev => ({ ...prev, [doc.id]: false }));
399+
}
400+
};
401+
402+
const toggleExpand = (doc: UploadedDocument) => {
403+
const docId = doc.id;
353404
if (expandedDocId === docId) {
354405
setExpandedDocId(null);
355406
return;
356407
}
357408
setExpandedDocId(docId);
409+
fetchPageIndexTreeForDoc(doc);
358410
};
359411

360412
// ─── Delete ───────────────────────────────────────────────
@@ -371,6 +423,16 @@ export default function DocumentsView({ isDark = true }: { isDark?: boolean }) {
371423
delete next[docId];
372424
return next;
373425
});
426+
setPageIndexLoadingByDocId(prev => {
427+
const next = { ...prev };
428+
delete next[docId];
429+
return next;
430+
});
431+
setPageIndexErrorByDocId(prev => {
432+
const next = { ...prev };
433+
delete next[docId];
434+
return next;
435+
});
374436
setPageIndexDocIdByDocId(prev => {
375437
const next = { ...prev };
376438
delete next[docId];
@@ -710,8 +772,10 @@ export default function DocumentsView({ isDark = true }: { isDark?: boolean }) {
710772
{documents.map((doc) => {
711773
const treeForDoc = pageIndexByDocId[doc.id];
712774
const nodeCount = treeForDoc ? pageIndexService.countNodes(treeForDoc) : 0;
713-
const maxPage = treeForDoc ? getMaxPageIndex(treeForDoc) : 0;
714-
const pageIndexId = pageIndexDocIdByDocId[doc.id];
775+
const pageCount = treeForDoc ? getPageCount(treeForDoc) : null;
776+
const pageIndexId = resolvePageIndexDocId(doc);
777+
const isTreeLoading = !!pageIndexLoadingByDocId[doc.id];
778+
const treeError = pageIndexErrorByDocId[doc.id];
715779

716780
return (
717781
<div key={doc.id}>
@@ -724,7 +788,7 @@ export default function DocumentsView({ isDark = true }: { isDark?: boolean }) {
724788
<div className="flex items-center gap-4">
725789
{/* Expand toggle */}
726790
<button
727-
onClick={() => toggleExpand(doc.id)}
791+
onClick={() => toggleExpand(doc)}
728792
className={cn(
729793
"p-1 rounded-md transition-colors cursor-pointer",
730794
isDark ? "hover:bg-white/10" : "hover:bg-gray-100"
@@ -860,13 +924,13 @@ export default function DocumentsView({ isDark = true }: { isDark?: boolean }) {
860924
{/* Stats bar */}
861925
<div className={cn("flex flex-wrap items-center gap-4 text-xs pb-3 border-b", isDark ? "border-white/5" : "border-gray-200")}>
862926
<span className={textSecondary}>
863-
<strong className={textPrimary}>{treeForDoc ? nodeCount : 'N/A'}</strong> nodes
927+
<strong className={textPrimary}>{treeForDoc ? nodeCount : (isTreeLoading ? 'Loading' : 'N/A')}</strong> nodes
864928
</span>
865929
<span className={textSecondary}>
866-
<strong className={textPrimary}>{treeForDoc ? treeForDoc.length : 'N/A'}</strong> sections
930+
<strong className={textPrimary}>{treeForDoc ? treeForDoc.length : (isTreeLoading ? 'Loading' : 'N/A')}</strong> sections
867931
</span>
868932
<span className={textSecondary}>
869-
<strong className={textPrimary}>{treeForDoc ? (maxPage || 'N/A') : 'N/A'}</strong> pages
933+
<strong className={textPrimary}>{treeForDoc ? (pageCount ?? 'N/A') : (isTreeLoading ? 'Loading' : 'N/A')}</strong> pages
870934
</span>
871935
{pageIndexId && (
872936
<span className={cn(
@@ -897,6 +961,14 @@ export default function DocumentsView({ isDark = true }: { isDark?: boolean }) {
897961
</div>
898962
))}
899963
</div>
964+
) : treeError ? (
965+
<p className={cn("text-sm text-center py-6", textMuted)}>
966+
{treeError}
967+
</p>
968+
) : isTreeLoading ? (
969+
<p className={cn("text-sm text-center py-6", textMuted)}>
970+
Loading PageIndex structure...
971+
</p>
900972
) : (
901973
<p className={cn("text-sm text-center py-6", textMuted)}>
902974
This document hasn't been indexed with PageIndex yet. Re-upload using Vectorless Indexing to generate structure.

src/services/localLLMService.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { getGroqSettings, type GroqSettings } from '../components/DemoCall/GroqS
3636
// API calls routed via secure proxy
3737
import { proxyJSON, ProxyRoutes } from './proxyClient';
3838
import { ragService } from './ragService';
39+
import { pageIndexService } from './pageIndexService';
3940

4041
import log from '../utils/logger';
4142

@@ -90,7 +91,7 @@ const RAG_TOOLS = [
9091
function: {
9192
name: 'get_document_details',
9293
description:
93-
'Get detailed information from a specific uploaded document by searching for a keyword or topic within it. Use when you need precise details from a known document.',
94+
'Get detailed information from a specific uploaded document by searching for a keyword or topic within it. Prefer PageIndex structured nodes (with page references) when available.',
9495
parameters: {
9596
type: 'object',
9697
properties: {
@@ -626,7 +627,7 @@ class GroqLLMService {
626627

627628
// Build messages array for Groq API (OpenAI format)
628629
const groqMessages: Array<{ role: 'system' | 'user' | 'assistant' | 'tool'; content: string; tool_call_id?: string; name?: string; tool_calls?: GroqToolCall[] }> = [
629-
{ role: 'system' as const, content: systemPrompt + (hasKnowledgeBase ? '\n\nYou have access to a knowledge base. Use the search_knowledge_base tool when the caller asks about business-specific information. ALWAYS search before answering questions about products, services, policies, pricing, or procedures.' : '') },
630+
{ role: 'system' as const, content: systemPrompt + (hasKnowledgeBase ? '\n\nYou have access to a knowledge base. Use the search_knowledge_base tool when the caller asks about business-specific information. ALWAYS search before answering questions about products, services, policies, pricing, or procedures. Use get_document_details when the caller references a specific document or asks for precise details; it can return PageIndex results with page references when available.' : '') },
630631
...recentHistory.map(msg => ({
631632
role: (msg.speaker === 'agent' ? 'assistant' : 'user') as 'assistant' | 'user',
632633
content: msg.speaker === 'agent' ? this.cleanFinalAnswer(msg.text) : msg.text
@@ -668,19 +669,60 @@ class GroqLLMService {
668669

669670
if (name === 'get_document_details' && knowledgeBase.id) {
670671
const query = String(args.query || '');
672+
const documentName = typeof args.document_name === 'string' ? args.document_name : undefined;
671673
log.debug(`🔧 Tool call: get_document_details("${query}")`);
672674

675+
let pageIndexNote: string | undefined;
676+
try {
677+
const pageIndexDoc = await ragService.getLatestPageIndexDocument(knowledgeBase.id, documentName);
678+
if (pageIndexDoc?.pageindex_doc_id) {
679+
const tree = await pageIndexService.getTree(pageIndexDoc.pageindex_doc_id);
680+
const matches = pageIndexService.searchTree(tree, query, 5);
681+
682+
if (matches.length > 0) {
683+
return JSON.stringify({
684+
source: 'pageindex',
685+
document: {
686+
id: pageIndexDoc.id,
687+
name: pageIndexDoc.file_name,
688+
pageindex_doc_id: pageIndexDoc.pageindex_doc_id,
689+
},
690+
results: matches.map((m, i) => ({ rank: i + 1, ...m })),
691+
total: matches.length,
692+
});
693+
}
694+
695+
return JSON.stringify({
696+
source: 'pageindex',
697+
document: {
698+
id: pageIndexDoc.id,
699+
name: pageIndexDoc.file_name,
700+
pageindex_doc_id: pageIndexDoc.pageindex_doc_id,
701+
},
702+
results: [],
703+
total: 0,
704+
note: 'No matching nodes found in PageIndex.',
705+
});
706+
} else if (documentName) {
707+
pageIndexNote = 'No PageIndex document found for the requested name.';
708+
}
709+
} catch (err) {
710+
pageIndexNote = err instanceof Error ? err.message : 'PageIndex lookup failed';
711+
}
712+
673713
try {
674714
const docs = await ragService.searchRelevantContext(query, knowledgeBase.id, 5);
675715
if (docs && docs.length > 0) {
676716
return JSON.stringify({
677-
results: docs.map((content, i) => ({ rank: i + 1, content, source: args.document_name || 'knowledge base' })),
717+
source: 'knowledge_base',
718+
results: docs.map((content, i) => ({ rank: i + 1, content, source: documentName || 'knowledge base' })),
678719
total: docs.length,
720+
note: pageIndexNote,
679721
});
680722
}
681-
return JSON.stringify({ results: [], total: 0 });
723+
return JSON.stringify({ source: 'knowledge_base', results: [], total: 0, note: pageIndexNote });
682724
} catch (err) {
683-
return JSON.stringify({ error: 'Document search failed' });
725+
return JSON.stringify({ error: 'Document search failed', note: pageIndexNote });
684726
}
685727
}
686728

0 commit comments

Comments
 (0)