Spaces:
Sleeping
Sleeping
File size: 6,576 Bytes
f866820 |
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 |
import { useState } from 'react';
import { runQuery } from '../api/client';
import ResultCard from './ResultCard';
export default function QueryPanel({ accessToken }) {
const [query, setQuery] = useState('');
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [showDebug, setShowDebug] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
if (!query.trim()) return;
if (!accessToken) {
setError('Please connect to Dropbox first to enable zero-storage queries.');
return;
}
setLoading(true);
setError(null);
try {
const data = await runQuery(query, accessToken);
setResult(data);
if (data.error) {
setError(data.error);
}
} catch (err) {
setError(err.message);
}
setLoading(false);
};
const handleClear = () => {
setQuery('');
setResult(null);
setError(null);
setShowDebug(false);
};
return (
<div className="flex-1 p-6 overflow-auto bg-slate-900">
{/* Query Form */}
<form onSubmit={handleSubmit} className="mb-6">
<label className="block text-sm font-medium text-slate-300 mb-2">
Enter your question:
</label>
<div className="flex gap-3">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="What would you like to know?"
className="flex-1 px-4 py-3 border border-slate-600 rounded-lg bg-slate-800 text-slate-100 placeholder-slate-500 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1 focus:ring-offset-slate-900 focus:border-blue-500 transition-all duration-200"
/>
<button
type="submit"
disabled={loading || !query.trim()}
className="bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed shadow-sm transition-all duration-200"
>
{loading ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
Searching...
</span>
) : (
'Search'
)}
</button>
</div>
</form>
{/* Error Message */}
{error && (
<div
className="bg-red-900/30 border border-red-700 text-red-400 px-4 py-3 rounded-lg mb-6 flex items-start gap-3"
role="alert"
>
<svg className="w-5 h-5 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{error}</span>
</div>
)}
{/* Results */}
{result && !error && (
<div className="space-y-6">
{/* Results Header with Clear Button */}
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold text-slate-100">Answer</h2>
<button
type="button"
onClick={handleClear}
className="flex items-center gap-1.5 text-sm text-slate-400 hover:text-slate-200 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
Clear Results
</button>
</div>
{/* Answer Card */}
<div className="bg-slate-800 border border-slate-700 rounded-lg p-4 shadow-sm">
<p className="text-slate-200 whitespace-pre-wrap leading-relaxed">
{result.answer || 'No answer generated.'}
</p>
</div>
{/* Citations */}
{result.citations?.length > 0 && (
<div>
<h2 className="text-base font-semibold text-slate-100 mb-3">
Citations ({result.citations.length})
</h2>
<div className="space-y-3">
{result.citations.map((citation, idx) => (
<ResultCard key={citation.id || idx} citation={citation} />
))}
</div>
</div>
)}
{/* Debug Toggle */}
<div className="pt-2">
<button
type="button"
onClick={() => setShowDebug(!showDebug)}
className="flex items-center gap-2 text-sm text-slate-400 hover:text-slate-200 transition-colors"
>
<svg
className={`w-4 h-4 transition-transform duration-200 ${showDebug ? 'rotate-90' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
{showDebug ? 'Hide Debug Info' : 'Show Debug Info'}
</button>
{showDebug && (
<pre className="mt-3 bg-slate-950 text-slate-300 p-4 rounded-lg text-xs overflow-auto max-h-96 border border-slate-700">
{JSON.stringify(result, null, 2)}
</pre>
)}
</div>
</div>
)}
{/* Empty State - No query yet */}
{!result && !error && !loading && (
<div className="flex flex-col items-center justify-center py-16 text-center">
<svg className="w-16 h-16 text-slate-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<h3 className="text-lg font-medium text-slate-300 mb-2">Ask a Question</h3>
<p className="text-sm text-slate-500 max-w-sm">
{accessToken
? 'Enter a question above to search your indexed documents. Results will include relevant citations from your files.'
: 'Connect to Dropbox and index your files first, then ask questions here. Your documents are re-fetched at query time for true zero-storage privacy.'
}
</p>
</div>
)}
</div>
);
}
|