Fix document inspector UI issues and improve accessibility (#644)

* Fix document inspector UI issues and improve accessibility

- Fixed copy button visibility when summaries overflow
- Added proper flex constraints and text truncation
- Enhanced metadata display in footer with relevance scores
- Improved accessibility with ARIA labels and semantic HTML
- Added better loading states and error handling
- Enhanced visual feedback with motion effects

* Fix CodeRabbit review issues

- Fixed copy timeout race condition using functional setState
- Added role="option" for proper aria-selected semantics

* Fix document metadata inconsistency in manual selection

- Align manual selection with auto-selection pattern
- Use top-level fields as primary source with metadata as fallback
- Ensures consistent display of titles, sections, and URLs
This commit is contained in:
Wirasm 2025-09-12 17:41:12 +03:00 committed by GitHub
parent 94aed6b9fa
commit 3d5753f8a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 159 additions and 90 deletions

View File

@ -27,29 +27,36 @@ export const ContentViewer: React.FC<ContentViewerProps> = ({ selectedItem, onCo
return (
<div className="flex flex-col h-full">
{/* Content Header */}
<div className="p-4 border-b border-white/10 flex items-center justify-between flex-shrink-0">
<div className="flex items-center gap-3">
{selectedItem.type === "document" ? (
<>
{/* Content Header - Fixed with proper overflow handling */}
<div className="p-4 border-b border-white/10 flex items-center gap-3 flex-shrink-0">
{/* Icon and Metadata - Allow to grow and shrink with min-w-0 for proper truncation */}
<div className="flex items-center gap-3 min-w-0 flex-1">
{/* Icon - Fixed size, no shrink */}
<div className="flex-shrink-0">
{selectedItem.type === "document" ? (
<FileText className="w-5 h-5 text-cyan-400" />
<div>
<h4 className="text-sm font-medium text-white/90">
) : (
<Code className="w-5 h-5 text-green-400" />
)}
</div>
{/* Metadata Content - Can shrink with proper overflow */}
<div className="min-w-0 flex-1">
{selectedItem.type === "document" ? (
<>
<h4 className="text-sm font-medium text-white/90 truncate">
{selectedItem.metadata && "title" in selectedItem.metadata
? selectedItem.metadata.title || "Document"
: "Document"}
</h4>
{selectedItem.metadata && "section" in selectedItem.metadata && selectedItem.metadata.section && (
<p className="text-xs text-gray-500">{selectedItem.metadata.section}</p>
<p className="text-xs text-gray-500 truncate">{selectedItem.metadata.section}</p>
)}
</div>
</>
) : (
<>
<Code className="w-5 h-5 text-green-400" />
<div>
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 bg-green-500/10 text-green-400 text-xs font-mono rounded">
</>
) : (
<>
<div className="flex items-center gap-2 min-w-0">
<span className="px-2 py-0.5 bg-green-500/10 text-green-400 text-xs font-mono rounded flex-shrink-0">
{selectedItem.type === "code" && selectedItem.metadata && "language" in selectedItem.metadata
? selectedItem.metadata.language || "unknown"
: "unknown"}
@ -58,24 +65,28 @@ export const ContentViewer: React.FC<ContentViewerProps> = ({ selectedItem, onCo
selectedItem.metadata &&
"file_path" in selectedItem.metadata &&
selectedItem.metadata.file_path && (
<span className="text-xs text-gray-500 font-mono">{selectedItem.metadata.file_path}</span>
<span className="text-xs text-gray-500 font-mono truncate min-w-0">
{selectedItem.metadata.file_path}
</span>
)}
</div>
{selectedItem.type === "code" &&
selectedItem.metadata &&
"summary" in selectedItem.metadata &&
selectedItem.metadata.summary && (
<p className="text-xs text-gray-400 mt-1">{selectedItem.metadata.summary}</p>
<p className="text-xs text-gray-400 mt-1 line-clamp-2">{selectedItem.metadata.summary}</p>
)}
</div>
</>
)}
</>
)}
</div>
</div>
{/* Copy Button - Never shrinks, always visible */}
<Button
size="sm"
variant="ghost"
onClick={() => onCopy(selectedItem.content, selectedItem.id)}
className="text-gray-400 hover:text-white"
className="text-gray-400 hover:text-white flex-shrink-0"
>
{copiedId === selectedItem.id ? (
<>
@ -95,21 +106,45 @@ export const ContentViewer: React.FC<ContentViewerProps> = ({ selectedItem, onCo
<div className="flex-1 overflow-y-auto min-h-0 p-6 scrollbar-thin">
{selectedItem.type === "document" ? (
<div className="prose prose-invert max-w-none">
<pre className="whitespace-pre-wrap text-sm text-gray-300 font-sans">{selectedItem.content}</pre>
<pre className="whitespace-pre-wrap text-sm text-gray-300 font-sans leading-relaxed">
{selectedItem.content || "No content available"}
</pre>
</div>
) : (
<pre className="bg-black/30 border border-white/10 rounded-lg p-4 overflow-x-auto">
<code className="text-sm text-gray-300 font-mono">{selectedItem.content}</code>
</pre>
<div className="relative">
<pre className="bg-black/30 border border-white/10 rounded-lg p-4 overflow-x-auto scrollbar-thin scrollbar-thumb-white/10 scrollbar-track-transparent">
<code className="text-sm text-gray-300 font-mono">
{selectedItem.content || "// No code content available"}
</code>
</pre>
</div>
)}
</div>
{/* Content Footer */}
{selectedItem.metadata?.relevance_score != null && (
<div className="p-3 border-t border-white/10 text-xs text-gray-500 flex-shrink-0">
Relevance Score: {(selectedItem.metadata.relevance_score * 100).toFixed(0)}%
{/* Content Footer - Show metadata */}
<div className="border-t border-white/10 flex-shrink-0">
<div className="px-4 py-3 flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center gap-4">
{selectedItem.metadata?.relevance_score != null && (
<span>
Relevance:{" "}
<span className="text-cyan-400">{(selectedItem.metadata.relevance_score * 100).toFixed(0)}%</span>
</span>
)}
{selectedItem.type === "document" && "url" in selectedItem.metadata && selectedItem.metadata.url && (
<a
href={selectedItem.metadata.url}
target="_blank"
rel="noopener noreferrer"
className="text-cyan-400 hover:text-cyan-300 transition-colors underline"
>
View Source
</a>
)}
</div>
<span className="text-gray-600">{selectedItem.type === "document" ? "Document Chunk" : "Code Example"}</span>
</div>
)}
</div>
</div>
);
};

View File

@ -61,16 +61,20 @@ export const InspectorSidebar: React.FC<InspectorSidebarProps> = ({
};
return (
<div className="w-80 border-r border-white/10 flex flex-col bg-black/40">
<aside className="w-80 border-r border-white/10 flex flex-col bg-black/40" aria-label="Document and code browser">
{/* Search */}
<div className="p-4 border-b border-white/10">
<div className="p-4 border-b border-white/10 flex-shrink-0">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<Search
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500 pointer-events-none"
aria-hidden="true"
/>
<Input
placeholder={`Search ${viewMode}...`}
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-9 bg-black/30"
aria-label={`Search ${viewMode}`}
/>
</div>
</div>
@ -78,9 +82,15 @@ export const InspectorSidebar: React.FC<InspectorSidebarProps> = ({
{/* Item List */}
<div className="flex-1 overflow-y-auto min-h-0 scrollbar-thin">
{isLoading ? (
<div className="p-4 text-center text-gray-500">Loading...</div>
<div className="p-4 text-center text-gray-500" aria-live="polite">
<Loader2 className="w-5 h-5 animate-spin mx-auto mb-2" aria-hidden="true" />
<span>Loading {viewMode}...</span>
</div>
) : items.length === 0 ? (
<div className="p-4 text-center text-gray-500">No {viewMode} found</div>
<div className="p-4 text-center text-gray-500">
No {viewMode} found
{searchQuery && <p className="text-xs mt-1">Try adjusting your search</p>}
</div>
) : (
<div className="p-2">
{items.map((item) => (
@ -88,34 +98,47 @@ export const InspectorSidebar: React.FC<InspectorSidebarProps> = ({
type="button"
key={item.id}
whileHover={{ x: 2 }}
whileTap={{ scale: 0.98 }}
onClick={() => onItemSelect(item)}
className={cn(
"w-full text-left p-3 rounded-lg mb-1 transition-colors",
"hover:bg-white/5",
selectedItemId === item.id ? "bg-cyan-500/10 border border-cyan-500/30" : "border border-transparent",
"w-full text-left p-3 rounded-lg mb-1 transition-all",
"hover:bg-white/5 focus:outline-none focus:ring-2 focus:ring-cyan-500/50",
selectedItemId === item.id
? "bg-cyan-500/10 border border-cyan-500/30 ring-1 ring-cyan-500/20"
: "border border-transparent",
)}
role="option"
aria-selected={selectedItemId === item.id}
aria-label={`${getItemTitle(item)}. ${getItemDescription(item)}`}
>
<div className="flex items-start gap-3">
<div className="mt-0.5">
{/* Icon - Fixed size */}
<div className="mt-0.5 flex-shrink-0" aria-hidden="true">
{viewMode === "documents" ? (
<FileText className="w-4 h-4 text-cyan-400" />
) : (
<Code className="w-4 h-4 text-green-400" />
)}
</div>
{/* Content - Can shrink with proper overflow */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-sm font-medium text-white/90 truncate">{getItemTitle(item)}</span>
<div className="flex items-center gap-2 mb-1 min-w-0">
<span className="text-sm font-medium text-white/90 truncate flex-1" title={getItemTitle(item)}>
{getItemTitle(item)}
</span>
{viewMode === "code" && (item as CodeExample).language && (
<span className="px-1.5 py-0.5 bg-green-500/10 text-green-400 text-xs rounded">
<span className="px-1.5 py-0.5 bg-green-500/10 text-green-400 text-xs rounded flex-shrink-0">
{(item as CodeExample).language}
</span>
)}
</div>
<p className="text-xs text-gray-500 line-clamp-2">{getItemDescription(item)}</p>
<p className="text-xs text-gray-500 line-clamp-2" title={getItemDescription(item)}>
{getItemDescription(item)}
</p>
{item.metadata?.relevance_score != null && (
<div className="flex items-center gap-1 mt-1">
<Hash className="w-3 h-3 text-gray-600" />
<Hash className="w-3 h-3 text-gray-600" aria-hidden="true" />
<span className="text-xs text-gray-600">
{(item.metadata.relevance_score * 100).toFixed(0)}%
</span>
@ -128,21 +151,25 @@ export const InspectorSidebar: React.FC<InspectorSidebarProps> = ({
{/* Load More Button */}
{hasNextPage && !isLoading && (
<div className="p-3 border-t border-white/10">
<div className="p-3 mt-2">
<Button
variant="ghost"
size="sm"
onClick={onLoadMore}
disabled={isFetchingNextPage}
className="w-full text-cyan-400 hover:text-white hover:bg-cyan-500/10"
className="w-full text-cyan-400 hover:text-white hover:bg-cyan-500/10 transition-all"
aria-label={`Load more ${viewMode}`}
>
{isFetchingNextPage ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Loading...
<Loader2 className="w-4 h-4 mr-2 animate-spin" aria-hidden="true" />
<span>Loading...</span>
</>
) : (
`Load More ${viewMode}`
<>
<span>Load More {viewMode}</span>
<span className="sr-only">. Press to load additional items.</span>
</>
)}
</Button>
</div>
@ -150,6 +177,6 @@ export const InspectorSidebar: React.FC<InspectorSidebarProps> = ({
</div>
)}
</div>
</div>
</aside>
);
};

View File

@ -3,7 +3,7 @@
* Orchestrates split-view design with sidebar navigation and content viewer
*/
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { InspectorDialog, InspectorDialogContent, InspectorDialogTitle } from "../../../ui/primitives";
import type { CodeExample, DocumentChunk, InspectorSelectedItem, KnowledgeItem } from "../../types";
import { useInspectorPagination } from "../hooks/useInspectorPagination";
@ -79,49 +79,56 @@ export const KnowledgeInspector: React.FC<KnowledgeInspectorProps> = ({ item, op
}
}, [viewMode, currentItems, selectedItem]);
const handleCopy = async (text: string, id: string) => {
await navigator.clipboard.writeText(text);
setCopiedId(id);
setTimeout(() => setCopiedId(null), 2000);
};
const handleItemSelect = (item: DocumentChunk | CodeExample) => {
if (viewMode === "documents") {
const doc = item as DocumentChunk;
setSelectedItem({
type: "document",
id: doc.id || "",
content: doc.content || "",
metadata: {
title: doc.metadata?.title,
section: doc.metadata?.section,
relevance_score: doc.metadata?.relevance_score,
url: doc.metadata?.url,
tags: doc.metadata?.tags,
},
});
} else {
const code = item as CodeExample;
setSelectedItem({
type: "code",
id: String(code.id),
content: code.content || code.code || "",
metadata: {
language: code.language,
file_path: code.file_path,
summary: code.summary,
relevance_score: code.metadata?.relevance_score,
title: code.title || code.example_name,
},
});
const handleCopy = useCallback(async (text: string, id: string) => {
try {
await navigator.clipboard.writeText(text);
setCopiedId(id);
setTimeout(() => setCopiedId((v) => (v === id ? null : v)), 2000);
} catch (error) {
console.error("Failed to copy to clipboard:", error);
}
};
}, []);
const handleViewModeChange = (mode: ViewMode) => {
const handleItemSelect = useCallback(
(item: DocumentChunk | CodeExample) => {
if (viewMode === "documents") {
const doc = item as DocumentChunk;
setSelectedItem({
type: "document",
id: doc.id || "",
content: doc.content || "",
metadata: {
title: doc.title || doc.metadata?.title,
section: doc.section || doc.metadata?.section,
relevance_score: doc.metadata?.relevance_score,
url: doc.url || doc.metadata?.url,
tags: doc.metadata?.tags,
},
});
} else {
const code = item as CodeExample;
setSelectedItem({
type: "code",
id: String(code.id),
content: code.content || code.code || "",
metadata: {
language: code.language,
file_path: code.file_path,
summary: code.summary,
relevance_score: code.metadata?.relevance_score,
title: code.title || code.example_name,
},
});
}
},
[viewMode],
);
const handleViewModeChange = useCallback((mode: ViewMode) => {
setViewMode(mode);
setSelectedItem(null);
setSearchQuery("");
};
}, []);
return (
<InspectorDialog open={open} onOpenChange={onOpenChange}>