jblyons15-research-ui 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +34 -0
- package/src/index.tsx +463 -0
- package/tsconfig.json +17 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jblyons15-research-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React UI components for CommandCenter research system",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"research",
|
|
9
|
+
"react",
|
|
10
|
+
"components",
|
|
11
|
+
"ui"
|
|
12
|
+
],
|
|
13
|
+
"author": "Crossover Research",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"react": "^18.2.0",
|
|
17
|
+
"react-dom": "^18.2.0"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"jblyons15-research-sdk": "^1.0.0",
|
|
21
|
+
"jblyons15-research-types": "^1.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/react": "^18.2.0",
|
|
25
|
+
"@types/react-dom": "^18.2.0",
|
|
26
|
+
"react": "^18.2.0",
|
|
27
|
+
"react-dom": "^18.2.0",
|
|
28
|
+
"typescript": "^5.0.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"type-check": "tsc --noEmit"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
// @crossover/research-ui - Complete Package Definition
|
|
2
|
+
// React components for research requests, results, and history
|
|
3
|
+
|
|
4
|
+
import React, { useState, useEffect } from 'react'
|
|
5
|
+
import type {
|
|
6
|
+
ResearchRequest,
|
|
7
|
+
ResearchResult,
|
|
8
|
+
ResearchJobStatus,
|
|
9
|
+
ResearchStatus,
|
|
10
|
+
ResearchError,
|
|
11
|
+
} from '@crossover/research-types'
|
|
12
|
+
import { ResearchOrchestrator } from '@crossover/research-sdk'
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// HOOK: useResearchOrchestrator
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export function useResearchOrchestrator(config?: {
|
|
19
|
+
perplexityApiKey?: string
|
|
20
|
+
firecrawlApiKey?: string
|
|
21
|
+
supabaseUrl?: string
|
|
22
|
+
supabaseAnonKey?: string
|
|
23
|
+
}): ResearchOrchestrator {
|
|
24
|
+
const [orchestrator] = useState(() => {
|
|
25
|
+
// In real usage, get these from environment or context
|
|
26
|
+
return new ResearchOrchestrator({
|
|
27
|
+
perplexityApiKey: config?.perplexityApiKey || process.env.REACT_APP_PERPLEXITY_KEY || '',
|
|
28
|
+
firecrawlApiKey: config?.firecrawlApiKey || process.env.REACT_APP_FIRECRAWL_KEY || '',
|
|
29
|
+
supabaseUrl: config?.supabaseUrl || process.env.REACT_APP_SUPABASE_URL || '',
|
|
30
|
+
supabaseAnonKey: config?.supabaseAnonKey || process.env.REACT_APP_SUPABASE_ANON_KEY || '',
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return orchestrator
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// HOOK: useResearchStatus - Real-time job status polling
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
export function useResearchStatus(jobId: string, orchestrator: ResearchOrchestrator) {
|
|
42
|
+
const [status, setStatus] = useState<ResearchJobStatus | null>(null)
|
|
43
|
+
const [loading, setLoading] = useState(true)
|
|
44
|
+
const [error, setError] = useState<ResearchError | null>(null)
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
let isMounted = true
|
|
48
|
+
let pollInterval: NodeJS.Timeout
|
|
49
|
+
|
|
50
|
+
const fetchStatus = async () => {
|
|
51
|
+
try {
|
|
52
|
+
const result = await orchestrator.getStatus(jobId)
|
|
53
|
+
if (isMounted) {
|
|
54
|
+
setStatus(result)
|
|
55
|
+
setLoading(false)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Continue polling if not completed
|
|
59
|
+
if (result && result.status !== 'completed' && result.status !== 'failed') {
|
|
60
|
+
pollInterval = setTimeout(fetchStatus, 2000) // Poll every 2s
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
if (isMounted) {
|
|
64
|
+
setError(err as ResearchError)
|
|
65
|
+
setLoading(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fetchStatus()
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
isMounted = false
|
|
74
|
+
if (pollInterval) clearTimeout(pollInterval)
|
|
75
|
+
}
|
|
76
|
+
}, [jobId, orchestrator])
|
|
77
|
+
|
|
78
|
+
return { status, loading, error }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// COMPONENT: RequestForm - Submit research requests
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
interface RequestFormProps {
|
|
86
|
+
onSubmit?: (jobId: string) => void
|
|
87
|
+
onError?: (error: ResearchError) => void
|
|
88
|
+
defaultCompany?: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function RequestForm({
|
|
92
|
+
onSubmit,
|
|
93
|
+
onError,
|
|
94
|
+
defaultCompany = '',
|
|
95
|
+
}: RequestFormProps) {
|
|
96
|
+
const orchestrator = useResearchOrchestrator()
|
|
97
|
+
const [loading, setLoading] = useState(false)
|
|
98
|
+
const [company, setCompany] = useState(defaultCompany)
|
|
99
|
+
const [questions, setQuestions] = useState<string[]>([''])
|
|
100
|
+
const [format, setFormat] = useState<'summary' | 'detailed' | 'brief'>('summary')
|
|
101
|
+
const [timeline, setTimeline] = useState<'urgent' | 'normal' | 'flexible'>('normal')
|
|
102
|
+
const [context, setContext] = useState('')
|
|
103
|
+
|
|
104
|
+
const handleAddQuestion = () => {
|
|
105
|
+
setQuestions([...questions, ''])
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const handleRemoveQuestion = (index: number) => {
|
|
109
|
+
setQuestions(questions.filter((_, i) => i !== index))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const handleQuestionChange = (index: number, value: string) => {
|
|
113
|
+
const updated = [...questions]
|
|
114
|
+
updated[index] = value
|
|
115
|
+
setQuestions(updated)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
119
|
+
e.preventDefault()
|
|
120
|
+
setLoading(true)
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const jobId = await orchestrator.submitRequest({
|
|
124
|
+
company,
|
|
125
|
+
questions: questions.filter(q => q.trim()),
|
|
126
|
+
format,
|
|
127
|
+
timeline,
|
|
128
|
+
context: context || undefined,
|
|
129
|
+
submittedBy: 'current-user-id', // Would get from auth context
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
onSubmit?.(jobId)
|
|
133
|
+
} catch (error) {
|
|
134
|
+
onError?.(error as ResearchError)
|
|
135
|
+
} finally {
|
|
136
|
+
setLoading(false)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<form onSubmit={handleSubmit} className="space-y-6">
|
|
142
|
+
<div>
|
|
143
|
+
<label className="block text-sm font-medium mb-2">Company Name</label>
|
|
144
|
+
<input
|
|
145
|
+
type="text"
|
|
146
|
+
value={company}
|
|
147
|
+
onChange={(e) => setCompany(e.target.value)}
|
|
148
|
+
required
|
|
149
|
+
placeholder="e.g., Acme Corp"
|
|
150
|
+
className="w-full px-3 py-2 border rounded-md"
|
|
151
|
+
/>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div>
|
|
155
|
+
<label className="block text-sm font-medium mb-2">Research Questions</label>
|
|
156
|
+
{questions.map((q, i) => (
|
|
157
|
+
<div key={i} className="flex gap-2 mb-2">
|
|
158
|
+
<input
|
|
159
|
+
type="text"
|
|
160
|
+
value={q}
|
|
161
|
+
onChange={(e) => handleQuestionChange(i, e.target.value)}
|
|
162
|
+
placeholder={`Question ${i + 1}`}
|
|
163
|
+
className="flex-1 px-3 py-2 border rounded-md"
|
|
164
|
+
/>
|
|
165
|
+
{questions.length > 1 && (
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
onClick={() => handleRemoveQuestion(i)}
|
|
169
|
+
className="px-3 py-2 bg-red-50 text-red-600 rounded-md"
|
|
170
|
+
>
|
|
171
|
+
Remove
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onClick={handleAddQuestion}
|
|
179
|
+
className="mt-2 px-3 py-2 bg-blue-50 text-blue-600 rounded-md"
|
|
180
|
+
>
|
|
181
|
+
Add Question
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div className="grid grid-cols-2 gap-4">
|
|
186
|
+
<div>
|
|
187
|
+
<label className="block text-sm font-medium mb-2">Format</label>
|
|
188
|
+
<select
|
|
189
|
+
value={format}
|
|
190
|
+
onChange={(e) => setFormat(e.target.value as any)}
|
|
191
|
+
className="w-full px-3 py-2 border rounded-md"
|
|
192
|
+
>
|
|
193
|
+
<option value="summary">Executive Summary</option>
|
|
194
|
+
<option value="detailed">Detailed Analysis</option>
|
|
195
|
+
<option value="brief">Quick Brief</option>
|
|
196
|
+
</select>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div>
|
|
200
|
+
<label className="block text-sm font-medium mb-2">Timeline</label>
|
|
201
|
+
<select
|
|
202
|
+
value={timeline}
|
|
203
|
+
onChange={(e) => setTimeline(e.target.value as any)}
|
|
204
|
+
className="w-full px-3 py-2 border rounded-md"
|
|
205
|
+
>
|
|
206
|
+
<option value="urgent">Urgent (24h)</option>
|
|
207
|
+
<option value="normal">Normal (3-5d)</option>
|
|
208
|
+
<option value="flexible">Flexible (1-2w)</option>
|
|
209
|
+
</select>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div>
|
|
214
|
+
<label className="block text-sm font-medium mb-2">Additional Context (optional)</label>
|
|
215
|
+
<textarea
|
|
216
|
+
value={context}
|
|
217
|
+
onChange={(e) => setContext(e.target.value)}
|
|
218
|
+
placeholder="Any additional details or constraints..."
|
|
219
|
+
rows={3}
|
|
220
|
+
className="w-full px-3 py-2 border rounded-md"
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<button
|
|
225
|
+
type="submit"
|
|
226
|
+
disabled={loading}
|
|
227
|
+
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
|
|
228
|
+
>
|
|
229
|
+
{loading ? 'Submitting...' : 'Submit Research Request'}
|
|
230
|
+
</button>
|
|
231
|
+
</form>
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// COMPONENT: ResultsViewer - Display research results
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
interface ResultsViewerProps {
|
|
240
|
+
jobId: string
|
|
241
|
+
orchestrator: ResearchOrchestrator
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function ResultsViewer({ jobId, orchestrator }: ResultsViewerProps) {
|
|
245
|
+
const { status, loading, error } = useResearchStatus(jobId, orchestrator)
|
|
246
|
+
const [results, setResults] = useState<ResearchResult | null>(null)
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (status?.status === 'completed') {
|
|
250
|
+
orchestrator.getResults(jobId).then(setResults)
|
|
251
|
+
}
|
|
252
|
+
}, [status?.status, jobId, orchestrator])
|
|
253
|
+
|
|
254
|
+
if (loading) {
|
|
255
|
+
return <div className="p-4 text-center">Loading...</div>
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (error) {
|
|
259
|
+
return (
|
|
260
|
+
<div className="p-4 bg-red-50 border border-red-200 rounded-md">
|
|
261
|
+
<p className="text-red-600 font-medium">Error: {error.message}</p>
|
|
262
|
+
{error.details && <pre className="mt-2 text-sm">{JSON.stringify(error.details, null, 2)}</pre>}
|
|
263
|
+
</div>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!status) {
|
|
268
|
+
return <div className="p-4 text-center">No results found</div>
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div className="space-y-6">
|
|
273
|
+
{/* Status */}
|
|
274
|
+
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
|
|
275
|
+
<div className="flex items-center justify-between">
|
|
276
|
+
<div>
|
|
277
|
+
<p className="font-medium">Status: {status.status}</p>
|
|
278
|
+
<p className="text-sm text-gray-600">Progress: {status.progress}%</p>
|
|
279
|
+
</div>
|
|
280
|
+
<div className="w-32 h-2 bg-gray-300 rounded-full overflow-hidden">
|
|
281
|
+
<div
|
|
282
|
+
className="h-full bg-blue-600 transition-all"
|
|
283
|
+
style={{ width: `${status.progress}%` }}
|
|
284
|
+
/>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
{/* Results */}
|
|
290
|
+
{results && (
|
|
291
|
+
<>
|
|
292
|
+
{/* Synthesis */}
|
|
293
|
+
<div>
|
|
294
|
+
<h3 className="text-lg font-medium mb-2">Research Synthesis</h3>
|
|
295
|
+
<div className="p-4 bg-gray-50 border rounded-md">
|
|
296
|
+
<p className="text-sm">{results.synthesis}</p>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
|
|
300
|
+
{/* Sources */}
|
|
301
|
+
<div>
|
|
302
|
+
<h3 className="text-lg font-medium mb-2">Sources</h3>
|
|
303
|
+
<div className="space-y-2">
|
|
304
|
+
{results.sources.map((source, i) => (
|
|
305
|
+
<div key={i} className="p-3 bg-gray-50 border rounded-md">
|
|
306
|
+
<a href={source.url} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline font-medium">
|
|
307
|
+
{source.title}
|
|
308
|
+
</a>
|
|
309
|
+
<p className="text-xs text-gray-500 mt-1">{source.url}</p>
|
|
310
|
+
<p className="text-sm text-gray-700 mt-1">{source.snippet}</p>
|
|
311
|
+
<p className="text-xs text-gray-500 mt-1">Confidence: {(source.confidence * 100).toFixed(0)}%</p>
|
|
312
|
+
</div>
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
{/* Metadata */}
|
|
318
|
+
<div className="grid grid-cols-3 gap-4">
|
|
319
|
+
<div className="p-3 bg-gray-50 border rounded-md">
|
|
320
|
+
<p className="text-xs text-gray-500">Cost</p>
|
|
321
|
+
<p className="font-medium">${results.cost.toFixed(2)}</p>
|
|
322
|
+
</div>
|
|
323
|
+
<div className="p-3 bg-gray-50 border rounded-md">
|
|
324
|
+
<p className="text-xs text-gray-500">Latency</p>
|
|
325
|
+
<p className="font-medium">{results.latency_ms}ms</p>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="p-3 bg-gray-50 border rounded-md">
|
|
328
|
+
<p className="text-xs text-gray-500">Confidence</p>
|
|
329
|
+
<p className="font-medium">{(results.confidence * 100).toFixed(0)}%</p>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Export */}
|
|
334
|
+
<div className="flex gap-2">
|
|
335
|
+
<button
|
|
336
|
+
onClick={() => {
|
|
337
|
+
const json = JSON.stringify(results, null, 2)
|
|
338
|
+
const blob = new Blob([json], { type: 'application/json' })
|
|
339
|
+
const url = URL.createObjectURL(blob)
|
|
340
|
+
const a = document.createElement('a')
|
|
341
|
+
a.href = url
|
|
342
|
+
a.download = `research-${jobId}.json`
|
|
343
|
+
a.click()
|
|
344
|
+
}}
|
|
345
|
+
className="px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700"
|
|
346
|
+
>
|
|
347
|
+
Export JSON
|
|
348
|
+
</button>
|
|
349
|
+
</div>
|
|
350
|
+
</>
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// COMPONENT: ResearchCard - Compact research result display
|
|
358
|
+
// ============================================================================
|
|
359
|
+
|
|
360
|
+
interface ResearchCardProps {
|
|
361
|
+
jobId: string
|
|
362
|
+
company: string
|
|
363
|
+
status: ResearchStatus
|
|
364
|
+
progress: number
|
|
365
|
+
onClick?: () => void
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function ResearchCard({
|
|
369
|
+
jobId,
|
|
370
|
+
company,
|
|
371
|
+
status,
|
|
372
|
+
progress,
|
|
373
|
+
onClick,
|
|
374
|
+
}: ResearchCardProps) {
|
|
375
|
+
const statusColor = {
|
|
376
|
+
pending: 'bg-gray-100 text-gray-700',
|
|
377
|
+
running: 'bg-blue-100 text-blue-700',
|
|
378
|
+
completed: 'bg-green-100 text-green-700',
|
|
379
|
+
failed: 'bg-red-100 text-red-700',
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<div
|
|
384
|
+
onClick={onClick}
|
|
385
|
+
className="p-4 border rounded-md hover:bg-gray-50 cursor-pointer transition"
|
|
386
|
+
>
|
|
387
|
+
<div className="flex items-start justify-between">
|
|
388
|
+
<div>
|
|
389
|
+
<h4 className="font-medium">{company}</h4>
|
|
390
|
+
<p className="text-xs text-gray-500">ID: {jobId.slice(0, 8)}...</p>
|
|
391
|
+
</div>
|
|
392
|
+
<span className={`px-2 py-1 rounded text-xs font-medium ${statusColor[status]}`}>
|
|
393
|
+
{status}
|
|
394
|
+
</span>
|
|
395
|
+
</div>
|
|
396
|
+
<div className="mt-2 w-full h-2 bg-gray-300 rounded-full overflow-hidden">
|
|
397
|
+
<div
|
|
398
|
+
className="h-full bg-blue-600 transition-all"
|
|
399
|
+
style={{ width: `${progress}%` }}
|
|
400
|
+
/>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ============================================================================
|
|
407
|
+
// COMPONENT: HistoryDashboard - List of research requests
|
|
408
|
+
// ============================================================================
|
|
409
|
+
|
|
410
|
+
interface HistoryDashboardProps {
|
|
411
|
+
orchestrator: ResearchOrchestrator
|
|
412
|
+
onSelectJob?: (jobId: string) => void
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function HistoryDashboard({
|
|
416
|
+
orchestrator,
|
|
417
|
+
onSelectJob,
|
|
418
|
+
}: HistoryDashboardProps) {
|
|
419
|
+
const [jobs, setJobs] = useState<Array<{
|
|
420
|
+
jobId: string
|
|
421
|
+
company: string
|
|
422
|
+
status: ResearchStatus
|
|
423
|
+
progress: number
|
|
424
|
+
}>>([])
|
|
425
|
+
|
|
426
|
+
// This would normally fetch from your backend
|
|
427
|
+
// For now it's a placeholder
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<div>
|
|
431
|
+
<h2 className="text-2xl font-bold mb-4">Research History</h2>
|
|
432
|
+
{jobs.length === 0 ? (
|
|
433
|
+
<div className="p-4 text-center text-gray-500">No research requests yet</div>
|
|
434
|
+
) : (
|
|
435
|
+
<div className="grid gap-4">
|
|
436
|
+
{jobs.map((job) => (
|
|
437
|
+
<ResearchCard
|
|
438
|
+
key={job.jobId}
|
|
439
|
+
jobId={job.jobId}
|
|
440
|
+
company={job.company}
|
|
441
|
+
status={job.status}
|
|
442
|
+
progress={job.progress}
|
|
443
|
+
onClick={() => onSelectJob?.(job.jobId)}
|
|
444
|
+
/>
|
|
445
|
+
))}
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
</div>
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ============================================================================
|
|
453
|
+
// EXPORTS
|
|
454
|
+
// ============================================================================
|
|
455
|
+
|
|
456
|
+
export default {
|
|
457
|
+
useResearchOrchestrator,
|
|
458
|
+
useResearchStatus,
|
|
459
|
+
RequestForm,
|
|
460
|
+
ResultsViewer,
|
|
461
|
+
ResearchCard,
|
|
462
|
+
HistoryDashboard,
|
|
463
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"rootDir": "src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"moduleResolution": "bundler"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|