Spaces:
Sleeping
Sleeping
File size: 7,696 Bytes
3786a3f 2fb781f 3786a3f 2fb781f 3786a3f 2fb781f 3786a3f 2fb781f 3786a3f 2fb781f 3786a3f 2fb781f 3786a3f 2fb781f 3786a3f 2fb781f 3786a3f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | 'use client';
import { ChevronRight, GitBranch, ArrowRight, Copy, Check } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { useState } from 'react';
export interface Hop {
from: string;
rel: string;
to: string;
doc?: string;
}
interface PathViewProps {
hops: Hop[];
conclusion?: string;
alternativePaths?: Hop[][];
hopCount?: number;
entityA?: string;
entityB?: string;
standalone?: boolean;
}
export function PathView({
hops,
conclusion,
alternativePaths = [],
hopCount,
entityA,
entityB,
standalone = false,
}: PathViewProps) {
const [copied, setCopied] = useState(false);
const [activeTab, setActiveTab] = useState(0);
if (!hops || hops.length === 0) return null;
const totalHops = hopCount ?? hops.length;
const handleCopy = () => {
const text = hops
.map((h) => `${h.from} --[${h.rel.replace(/_/g, ' ')}]--> ${h.to}`)
.join('\n');
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const renderChain = (chain: Hop[], index?: number) => (
<div key={index ?? 'main'} className="space-y-2">
{index !== undefined && (
<p className="text-xs font-medium text-text-muted">Alternative Path {index + 1}</p>
)}
<div
className={cn(
'flex flex-wrap items-stretch gap-1.5 overflow-x-auto rounded-md border border-border bg-bg-base p-3 scrollbar-thin',
index !== undefined && 'border-accent-indigo/30 bg-accent-indigo/5'
)}
>
{chain.map((hop, i) => (
<div key={i} className="flex items-center gap-1.5">
<div className="flex flex-col items-center rounded-md bg-bg-elevated px-3 py-2">
<span className="text-sm font-semibold text-text-primary">{hop.from}</span>
{hop.doc && <span className="text-[10px] text-text-muted">{hop.doc}</span>}
</div>
<div className="flex flex-col items-center px-1">
<ChevronRight className="h-4 w-4 text-text-muted" />
<span className="whitespace-nowrap text-[10px] font-medium uppercase text-accent-cyan">
{hop.rel.replace(/_/g, ' ')}
</span>
</div>
{i === chain.length - 1 && (
<div className="flex flex-col items-center rounded-md bg-accent-violet/10 px-3 py-2">
<span className="text-sm font-semibold text-accent-violet">{hop.to}</span>
</div>
)}
</div>
))}
</div>
</div>
);
const renderStandalone = () => (
<div className="space-y-4 rounded-lg border border-accent-cyan/30 bg-accent-cyan/5 p-5">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm font-semibold text-accent-cyan">
<GitBranch className="h-4 w-4" /> Multi-Hop Reasoning Path
</div>
<div className="flex items-center gap-2">
<Badge variant="info">{totalHops} hop{totalHops !== 1 ? 's' : ''}</Badge>
<button
onClick={handleCopy}
className="flex items-center gap-1 rounded-md px-2 py-1 text-xs text-text-muted hover:bg-bg-elevated hover:text-text-primary transition-colors"
>
{copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
{copied ? 'Copied' : 'Copy'}
</button>
</div>
</div>
{/* Entity pair header */}
{entityA && entityB && (
<div className="flex items-center gap-2 rounded-md bg-bg-elevated px-3 py-2">
<span className="text-sm font-medium text-text-primary">{entityA}</span>
<ArrowRight className="h-4 w-4 text-text-muted" />
<span className="text-sm font-medium text-accent-violet">{entityB}</span>
</div>
)}
{/* Vertical timeline layout */}
<div className="space-y-0">
{hops.map((hop, i) => (
<div key={i} className="flex gap-3">
{/* Timeline line */}
<div className="flex flex-col items-center">
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-accent-cyan/20 text-xs font-bold text-accent-cyan">
{i + 1}
</div>
{i < hops.length - 1 && (
<div className="w-0.5 flex-1 bg-accent-cyan/20" />
)}
</div>
{/* Hop content */}
<div className="flex-1 pb-4">
<div className="rounded-md border border-border bg-bg-base p-3">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-semibold text-text-primary">{hop.from}</span>
<span className="rounded-md bg-accent-cyan/10 px-2 py-0.5 text-[10px] font-medium uppercase text-accent-cyan">
{hop.rel.replace(/_/g, ' ')}
</span>
<span className="text-sm font-semibold text-accent-violet">{hop.to}</span>
</div>
{hop.doc && (
<p className="mt-1 text-[10px] text-text-muted">Source: {hop.doc}</p>
)}
</div>
</div>
</div>
))}
</div>
{/* Alternative paths tabs */}
{alternativePaths.length > 0 && (
<div className="space-y-2">
<div className="flex gap-2 border-b border-border">
<button
onClick={() => setActiveTab(0)}
className={cn(
'px-3 py-1.5 text-xs font-medium border-b-2 transition-colors',
activeTab === 0
? 'border-accent-cyan text-accent-cyan'
: 'border-transparent text-text-muted hover:text-text-primary'
)}
>
Primary Path
</button>
{alternativePaths.map((_, i) => (
<button
key={i}
onClick={() => setActiveTab(i + 1)}
className={cn(
'px-3 py-1.5 text-xs font-medium border-b-2 transition-colors',
activeTab === i + 1
? 'border-accent-indigo text-accent-indigo'
: 'border-transparent text-text-muted hover:text-text-primary'
)}
>
Path {i + 2}
</button>
))}
</div>
{activeTab === 0 && renderChain(hops)}
{activeTab > 0 && renderChain(alternativePaths[activeTab - 1], activeTab)}
</div>
)}
{/* Conclusion */}
{conclusion && (
<div className="rounded-md border border-success/30 bg-success/10 p-3 text-sm text-text-primary">
<span className="font-semibold text-success">Conclusion: </span>
{conclusion}
</div>
)}
</div>
);
// Compact mode (default — used in Query page)
return (
<div className="space-y-3 rounded-md border border-accent-cyan/30 bg-accent-cyan/5 p-4">
<div className="flex items-center gap-2 text-sm font-semibold text-accent-cyan">
<GitBranch className="h-4 w-4" /> Multi-Hop Reasoning Path
</div>
{renderChain(hops)}
{alternativePaths.length > 0 &&
alternativePaths.map((p, i) => renderChain(p, i + 1))}
{conclusion && (
<div className="rounded-md border border-success/30 bg-success/10 p-3 text-sm text-text-primary">
<span className="font-semibold text-success">Conclusion: </span>
{conclusion}
</div>
)}
</div>
);
}
|