voyageai-cli 1.23.1 → 1.26.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/README.md +64 -0
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +1 -1
- package/src/commands/bug.js +1 -1
- package/src/commands/mcp-server.js +74 -0
- package/src/commands/playground.js +31 -0
- package/src/commands/scaffold.js +23 -1
- package/src/commands/workflow.js +336 -0
- package/src/lib/explanations.js +53 -0
- package/src/lib/scaffold-structure.js +8 -9
- package/src/lib/telemetry.js +1 -1
- package/src/lib/template-engine.js +240 -0
- package/src/lib/templates/nextjs/README.md.tpl +78 -55
- package/src/lib/templates/nextjs/favicon.svg.tpl +11 -0
- package/src/lib/templates/nextjs/footer.jsx.tpl +49 -0
- package/src/lib/templates/nextjs/layout.jsx.tpl +16 -10
- package/src/lib/templates/nextjs/lib-mongo.js.tpl +5 -5
- package/src/lib/templates/nextjs/lib-voyage.js.tpl +13 -8
- package/src/lib/templates/nextjs/navbar.jsx.tpl +98 -0
- package/src/lib/templates/nextjs/page-home.jsx.tpl +201 -0
- package/src/lib/templates/nextjs/page-search.jsx.tpl +184 -82
- package/src/lib/templates/nextjs/theme-registry.jsx.tpl +51 -0
- package/src/lib/templates/nextjs/theme.js.tpl +138 -65
- package/src/lib/templates/nextjs/vai-logo-256.png +0 -0
- package/src/lib/workflow-utils.js +65 -0
- package/src/lib/workflow.js +1259 -0
- package/src/mcp/install.js +201 -0
- package/src/mcp/tools/management.js +1 -60
- package/src/playground/icons/dark/128.png +0 -0
- package/src/playground/icons/dark/16.png +0 -0
- package/src/playground/icons/dark/256.png +0 -0
- package/src/playground/icons/dark/32.png +0 -0
- package/src/playground/icons/dark/64.png +0 -0
- package/src/playground/icons/light/128.png +0 -0
- package/src/playground/icons/light/16.png +0 -0
- package/src/playground/icons/light/256.png +0 -0
- package/src/playground/icons/light/32.png +0 -0
- package/src/playground/icons/light/64.png +0 -0
- package/src/playground/icons/watermark.png +0 -0
- package/src/playground/index.html +125 -73
- package/src/workflows/consistency-check.json +64 -0
- package/src/workflows/cost-analysis.json +69 -0
- package/src/workflows/multi-collection-search.json +80 -0
- package/src/workflows/research-and-summarize.json +46 -0
- package/src/workflows/smart-ingest.json +63 -0
|
@@ -6,16 +6,22 @@
|
|
|
6
6
|
* Dimensions: {{dimensions}}
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
function getApiUrl() {
|
|
10
|
+
return process.env.VOYAGE_API_URL || 'https://api.voyageai.com/v1';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getApiKey() {
|
|
14
|
+
const key = process.env.VOYAGE_API_KEY;
|
|
15
|
+
if (!key) throw new Error('VOYAGE_API_KEY environment variable is required');
|
|
16
|
+
return key;
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Generate embeddings for text(s) using Voyage AI.
|
|
14
21
|
*/
|
|
15
22
|
export async function embed(input, options = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
23
|
+
const VOYAGE_API_URL = getApiUrl();
|
|
24
|
+
const VOYAGE_API_KEY = getApiKey();
|
|
19
25
|
|
|
20
26
|
const texts = Array.isArray(input) ? input : [input];
|
|
21
27
|
|
|
@@ -66,9 +72,8 @@ export async function embedDocuments(documents, options = {}) {
|
|
|
66
72
|
* Rerank documents by relevance to a query.
|
|
67
73
|
*/
|
|
68
74
|
export async function rerank(query, documents, options = {}) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
75
|
+
const VOYAGE_API_URL = getApiUrl();
|
|
76
|
+
const VOYAGE_API_KEY = getApiKey();
|
|
72
77
|
|
|
73
78
|
const response = await fetch(`${VOYAGE_API_URL}/rerank`, {
|
|
74
79
|
method: 'POST',
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded AppBar with dark mode toggle
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AppBar,
|
|
10
|
+
Toolbar,
|
|
11
|
+
Typography,
|
|
12
|
+
IconButton,
|
|
13
|
+
Box,
|
|
14
|
+
Chip,
|
|
15
|
+
Tooltip,
|
|
16
|
+
useTheme,
|
|
17
|
+
} from '@mui/material';
|
|
18
|
+
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
|
19
|
+
import LightModeIcon from '@mui/icons-material/LightMode';
|
|
20
|
+
import GitHubIcon from '@mui/icons-material/GitHub';
|
|
21
|
+
import { useColorMode } from './ThemeRegistry';
|
|
22
|
+
|
|
23
|
+
export default function Navbar() {
|
|
24
|
+
const theme = useTheme();
|
|
25
|
+
const { mode, toggle } = useColorMode();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<AppBar position="sticky" elevation={0} color="transparent">
|
|
29
|
+
<Toolbar sx={{ gap: 1.5 }}>
|
|
30
|
+
{/* Logo / wordmark */}
|
|
31
|
+
<Box
|
|
32
|
+
component="img"
|
|
33
|
+
src="/vai-logo.png"
|
|
34
|
+
alt="vai logo"
|
|
35
|
+
sx={{ height: 28, width: 28 }}
|
|
36
|
+
/>
|
|
37
|
+
<Typography
|
|
38
|
+
variant="h6"
|
|
39
|
+
sx={{
|
|
40
|
+
fontWeight: 800,
|
|
41
|
+
letterSpacing: '-0.02em',
|
|
42
|
+
background: `linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.primary.dark} 100%)`,
|
|
43
|
+
WebkitBackgroundClip: 'text',
|
|
44
|
+
WebkitTextFillColor: 'transparent',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{{projectName}}
|
|
48
|
+
</Typography>
|
|
49
|
+
|
|
50
|
+
<Chip
|
|
51
|
+
label="vai"
|
|
52
|
+
size="small"
|
|
53
|
+
sx={{
|
|
54
|
+
fontWeight: 700,
|
|
55
|
+
fontSize: '0.65rem',
|
|
56
|
+
height: 20,
|
|
57
|
+
bgcolor: 'primary.main',
|
|
58
|
+
color: 'primary.contrastText',
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<Box sx={{ flex: 1 }} />
|
|
63
|
+
|
|
64
|
+
{/* Model badge */}
|
|
65
|
+
<Chip
|
|
66
|
+
label="{{model}}"
|
|
67
|
+
size="small"
|
|
68
|
+
variant="outlined"
|
|
69
|
+
sx={{
|
|
70
|
+
display: { xs: 'none', sm: 'flex' },
|
|
71
|
+
fontFamily: 'monospace',
|
|
72
|
+
fontSize: '0.7rem',
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
{/* GitHub link */}
|
|
77
|
+
<Tooltip title="View on GitHub">
|
|
78
|
+
<IconButton
|
|
79
|
+
size="small"
|
|
80
|
+
href="https://github.com/mrlynn/voyageai-cli"
|
|
81
|
+
target="_blank"
|
|
82
|
+
rel="noopener"
|
|
83
|
+
sx={{ color: 'text.secondary' }}
|
|
84
|
+
>
|
|
85
|
+
<GitHubIcon fontSize="small" />
|
|
86
|
+
</IconButton>
|
|
87
|
+
</Tooltip>
|
|
88
|
+
|
|
89
|
+
{/* Dark mode toggle */}
|
|
90
|
+
<Tooltip title={mode === 'dark' ? 'Light mode' : 'Dark mode'}>
|
|
91
|
+
<IconButton size="small" onClick={toggle} sx={{ color: 'text.secondary' }}>
|
|
92
|
+
{mode === 'dark' ? <LightModeIcon fontSize="small" /> : <DarkModeIcon fontSize="small" />}
|
|
93
|
+
</IconButton>
|
|
94
|
+
</Tooltip>
|
|
95
|
+
</Toolbar>
|
|
96
|
+
</AppBar>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Landing Page — branded hero with quick stats
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Box,
|
|
10
|
+
Container,
|
|
11
|
+
Typography,
|
|
12
|
+
Button,
|
|
13
|
+
Grid,
|
|
14
|
+
Card,
|
|
15
|
+
CardContent,
|
|
16
|
+
Stack,
|
|
17
|
+
Chip,
|
|
18
|
+
useTheme,
|
|
19
|
+
alpha,
|
|
20
|
+
} from '@mui/material';
|
|
21
|
+
import SearchIcon from '@mui/icons-material/Search';
|
|
22
|
+
import StorageIcon from '@mui/icons-material/Storage';
|
|
23
|
+
import BoltIcon from '@mui/icons-material/Bolt';
|
|
24
|
+
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
|
25
|
+
import Navbar from '@/components/Navbar';
|
|
26
|
+
import Footer from '@/components/Footer';
|
|
27
|
+
|
|
28
|
+
const features = [
|
|
29
|
+
{
|
|
30
|
+
icon: <AutoAwesomeIcon />,
|
|
31
|
+
title: 'Voyage AI Embeddings',
|
|
32
|
+
desc: 'State-of-the-art {{model}} model with {{dimensions}}-dimensional vectors for precise semantic understanding.',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
icon: <StorageIcon />,
|
|
36
|
+
title: 'MongoDB Atlas Vector Search',
|
|
37
|
+
desc: 'Lightning-fast approximate nearest-neighbor search on your {{db}}.{{collection}} collection.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
icon: <BoltIcon />,
|
|
41
|
+
title: 'Full RAG Pipeline',
|
|
42
|
+
desc: 'Ingest, chunk, embed, store, and search — a complete retrieval-augmented generation stack out of the box.',
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export default function HomePage() {
|
|
47
|
+
const theme = useTheme();
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
|
51
|
+
<Navbar />
|
|
52
|
+
|
|
53
|
+
{/* ── Hero ──────────────────────────────── */}
|
|
54
|
+
<Box
|
|
55
|
+
sx={{
|
|
56
|
+
position: 'relative',
|
|
57
|
+
overflow: 'hidden',
|
|
58
|
+
pt: { xs: 8, md: 14 },
|
|
59
|
+
pb: { xs: 8, md: 12 },
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{/* gradient accent */}
|
|
63
|
+
<Box
|
|
64
|
+
sx={{
|
|
65
|
+
position: 'absolute',
|
|
66
|
+
top: -120,
|
|
67
|
+
right: -120,
|
|
68
|
+
width: 480,
|
|
69
|
+
height: 480,
|
|
70
|
+
borderRadius: '50%',
|
|
71
|
+
background: `radial-gradient(circle, ${alpha(theme.palette.primary.main, 0.15)} 0%, transparent 70%)`,
|
|
72
|
+
pointerEvents: 'none',
|
|
73
|
+
}}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
<Container maxWidth="md" sx={{ position: 'relative', textAlign: 'center' }}>
|
|
77
|
+
<Box
|
|
78
|
+
component="img"
|
|
79
|
+
src="/vai-logo.png"
|
|
80
|
+
alt="vai logo"
|
|
81
|
+
sx={{ width: 64, height: 64, mb: 2, mx: 'auto', display: 'block' }}
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
<Chip
|
|
85
|
+
label="Scaffolded with vai"
|
|
86
|
+
size="small"
|
|
87
|
+
color="success"
|
|
88
|
+
sx={{ mb: 3 }}
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<Typography variant="h2" component="h1" gutterBottom>
|
|
92
|
+
<Box
|
|
93
|
+
component="span"
|
|
94
|
+
sx={{
|
|
95
|
+
background: `linear-gradient(135deg, ${theme.palette.primary.main}, ${theme.palette.primary.light})`,
|
|
96
|
+
WebkitBackgroundClip: 'text',
|
|
97
|
+
WebkitTextFillColor: 'transparent',
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{{projectName}}
|
|
101
|
+
</Box>
|
|
102
|
+
</Typography>
|
|
103
|
+
|
|
104
|
+
<Typography variant="h5" component="p" color="text.secondary" gutterBottom>
|
|
105
|
+
Semantic search powered by Voyage AI
|
|
106
|
+
</Typography>
|
|
107
|
+
|
|
108
|
+
<Typography
|
|
109
|
+
variant="h6"
|
|
110
|
+
color="text.secondary"
|
|
111
|
+
sx={{ maxWidth: 560, mx: 'auto', mb: 4, fontWeight: 400 }}
|
|
112
|
+
>
|
|
113
|
+
Ask questions in natural language and find the most relevant documents
|
|
114
|
+
using vector embeddings{{#if rerank}} with reranking{{/if}}.
|
|
115
|
+
</Typography>
|
|
116
|
+
|
|
117
|
+
<Stack direction="row" spacing={2} justifyContent="center">
|
|
118
|
+
<Button
|
|
119
|
+
variant="contained"
|
|
120
|
+
size="large"
|
|
121
|
+
href="/search"
|
|
122
|
+
startIcon={<SearchIcon />}
|
|
123
|
+
sx={{ px: 4 }}
|
|
124
|
+
>
|
|
125
|
+
Try Search
|
|
126
|
+
</Button>
|
|
127
|
+
<Button
|
|
128
|
+
variant="outlined"
|
|
129
|
+
size="large"
|
|
130
|
+
href="/api/health"
|
|
131
|
+
sx={{ px: 4 }}
|
|
132
|
+
>
|
|
133
|
+
API Health
|
|
134
|
+
</Button>
|
|
135
|
+
</Stack>
|
|
136
|
+
</Container>
|
|
137
|
+
</Box>
|
|
138
|
+
|
|
139
|
+
{/* ── Feature cards ────────────────────── */}
|
|
140
|
+
<Container maxWidth="lg" sx={{ pb: 10 }}>
|
|
141
|
+
<Grid container spacing={3}>
|
|
142
|
+
{features.map((f, i) => (
|
|
143
|
+
<Grid item xs={12} md={4} key={i}>
|
|
144
|
+
<Card sx={{ height: '100%' }}>
|
|
145
|
+
<CardContent sx={{ p: 3 }}>
|
|
146
|
+
<Box
|
|
147
|
+
sx={{
|
|
148
|
+
width: 48,
|
|
149
|
+
height: 48,
|
|
150
|
+
borderRadius: 2,
|
|
151
|
+
display: 'flex',
|
|
152
|
+
alignItems: 'center',
|
|
153
|
+
justifyContent: 'center',
|
|
154
|
+
mb: 2,
|
|
155
|
+
bgcolor: alpha(theme.palette.primary.main, 0.1),
|
|
156
|
+
color: 'primary.main',
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
{f.icon}
|
|
160
|
+
</Box>
|
|
161
|
+
<Typography variant="h6" gutterBottom>
|
|
162
|
+
{f.title}
|
|
163
|
+
</Typography>
|
|
164
|
+
<Typography variant="body2" color="text.secondary">
|
|
165
|
+
{f.desc}
|
|
166
|
+
</Typography>
|
|
167
|
+
</CardContent>
|
|
168
|
+
</Card>
|
|
169
|
+
</Grid>
|
|
170
|
+
))}
|
|
171
|
+
</Grid>
|
|
172
|
+
|
|
173
|
+
{/* ── Config overview ────────────────── */}
|
|
174
|
+
<Box
|
|
175
|
+
sx={{
|
|
176
|
+
mt: 6,
|
|
177
|
+
p: 3,
|
|
178
|
+
borderRadius: 3,
|
|
179
|
+
bgcolor: theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'grey.50',
|
|
180
|
+
border: `1px solid ${theme.palette.divider}`,
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
<Typography variant="overline" color="text.secondary" gutterBottom>
|
|
184
|
+
Configuration
|
|
185
|
+
</Typography>
|
|
186
|
+
<Stack direction="row" flexWrap="wrap" gap={1} sx={{ mt: 1 }}>
|
|
187
|
+
<Chip label="Model: {{model}}" size="small" variant="outlined" sx={{ fontFamily: 'monospace' }} />
|
|
188
|
+
<Chip label="Dims: {{dimensions}}" size="small" variant="outlined" sx={{ fontFamily: 'monospace' }} />
|
|
189
|
+
<Chip label="DB: {{db}}.{{collection}}" size="small" variant="outlined" sx={{ fontFamily: 'monospace' }} />
|
|
190
|
+
<Chip label="Index: {{index}}" size="small" variant="outlined" sx={{ fontFamily: 'monospace' }} />
|
|
191
|
+
{{#if rerank}}
|
|
192
|
+
<Chip label="Rerank: {{rerankModel}}" size="small" color="success" sx={{ fontFamily: 'monospace' }} />
|
|
193
|
+
{{/if}}
|
|
194
|
+
</Stack>
|
|
195
|
+
</Box>
|
|
196
|
+
</Container>
|
|
197
|
+
|
|
198
|
+
<Footer />
|
|
199
|
+
</Box>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Search Page
|
|
2
|
+
* Search Page — branded semantic search UI
|
|
3
3
|
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -14,21 +14,52 @@ import {
|
|
|
14
14
|
Card,
|
|
15
15
|
CardContent,
|
|
16
16
|
Typography,
|
|
17
|
-
List,
|
|
18
|
-
ListItem,
|
|
19
17
|
Chip,
|
|
20
18
|
CircularProgress,
|
|
21
19
|
Alert,
|
|
22
20
|
InputAdornment,
|
|
21
|
+
Stack,
|
|
22
|
+
LinearProgress,
|
|
23
|
+
Tooltip,
|
|
24
|
+
IconButton,
|
|
25
|
+
Fade,
|
|
26
|
+
useTheme,
|
|
27
|
+
alpha,
|
|
23
28
|
} from '@mui/material';
|
|
24
29
|
import SearchIcon from '@mui/icons-material/Search';
|
|
30
|
+
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
31
|
+
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
|
32
|
+
import Navbar from '@/components/Navbar';
|
|
33
|
+
import Footer from '@/components/Footer';
|
|
34
|
+
|
|
35
|
+
function ScoreBar({ score }) {
|
|
36
|
+
const pct = Math.round(score * 100);
|
|
37
|
+
const color = pct >= 80 ? 'success' : pct >= 50 ? 'primary' : 'warning';
|
|
38
|
+
return (
|
|
39
|
+
<Tooltip title={`Relevance: ${score.toFixed(4)}`}>
|
|
40
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 120 }}>
|
|
41
|
+
<LinearProgress
|
|
42
|
+
variant="determinate"
|
|
43
|
+
value={pct}
|
|
44
|
+
color={color}
|
|
45
|
+
sx={{ flex: 1, height: 6, borderRadius: 3 }}
|
|
46
|
+
/>
|
|
47
|
+
<Typography variant="caption" fontWeight={700} sx={{ fontFamily: 'monospace', minWidth: 36 }}>
|
|
48
|
+
{pct}%
|
|
49
|
+
</Typography>
|
|
50
|
+
</Box>
|
|
51
|
+
</Tooltip>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
25
54
|
|
|
26
55
|
export default function SearchPage() {
|
|
56
|
+
const theme = useTheme();
|
|
27
57
|
const [query, setQuery] = useState('');
|
|
28
58
|
const [results, setResults] = useState([]);
|
|
29
59
|
const [loading, setLoading] = useState(false);
|
|
30
60
|
const [error, setError] = useState(null);
|
|
31
61
|
const [meta, setMeta] = useState(null);
|
|
62
|
+
const [copied, setCopied] = useState(null);
|
|
32
63
|
|
|
33
64
|
const handleSearch = async (e) => {
|
|
34
65
|
e.preventDefault();
|
|
@@ -60,88 +91,159 @@ export default function SearchPage() {
|
|
|
60
91
|
}
|
|
61
92
|
};
|
|
62
93
|
|
|
94
|
+
const copyText = (text, idx) => {
|
|
95
|
+
navigator.clipboard.writeText(text);
|
|
96
|
+
setCopied(idx);
|
|
97
|
+
setTimeout(() => setCopied(null), 1500);
|
|
98
|
+
};
|
|
99
|
+
|
|
63
100
|
return (
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
value={query}
|
|
78
|
-
onChange={(e) => setQuery(e.target.value)}
|
|
79
|
-
placeholder="Enter your search query..."
|
|
80
|
-
variant="outlined"
|
|
81
|
-
InputProps={{
|
|
82
|
-
startAdornment: (
|
|
83
|
-
<InputAdornment position="start">
|
|
84
|
-
<SearchIcon />
|
|
85
|
-
</InputAdornment>
|
|
86
|
-
),
|
|
87
|
-
endAdornment: (
|
|
88
|
-
<InputAdornment position="end">
|
|
89
|
-
<Button
|
|
90
|
-
type="submit"
|
|
91
|
-
variant="contained"
|
|
92
|
-
disabled={loading || !query.trim()}
|
|
93
|
-
>
|
|
94
|
-
{loading ? <CircularProgress size={24} /> : 'Search'}
|
|
95
|
-
</Button>
|
|
96
|
-
</InputAdornment>
|
|
97
|
-
),
|
|
98
|
-
}}
|
|
99
|
-
/>
|
|
100
|
-
</Box>
|
|
101
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
|
|
102
|
+
<Navbar />
|
|
103
|
+
|
|
104
|
+
<Container maxWidth="md" sx={{ py: 5, flex: 1 }}>
|
|
105
|
+
{/* Header */}
|
|
106
|
+
<Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}>
|
|
107
|
+
<IconButton href="/" size="small" sx={{ color: 'text.secondary' }}>
|
|
108
|
+
<ArrowBackIcon fontSize="small" />
|
|
109
|
+
</IconButton>
|
|
110
|
+
<Typography variant="h4" component="h1" fontWeight={800}>
|
|
111
|
+
Semantic Search
|
|
112
|
+
</Typography>
|
|
113
|
+
</Stack>
|
|
101
114
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
<Typography variant="subtitle1" sx={{ mb: 4, pl: 5.5 }}>
|
|
116
|
+
Using <strong>{{model}}</strong> embeddings{{#if rerank}} with <strong>{{rerankModel}}</strong> reranking{{/if}}
|
|
117
|
+
</Typography>
|
|
118
|
+
|
|
119
|
+
{/* Search form */}
|
|
120
|
+
<Box component="form" onSubmit={handleSearch} sx={{ mb: 4 }}>
|
|
121
|
+
<TextField
|
|
122
|
+
fullWidth
|
|
123
|
+
value={query}
|
|
124
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
125
|
+
placeholder="Ask a question in natural language…"
|
|
126
|
+
variant="outlined"
|
|
127
|
+
autoFocus
|
|
128
|
+
InputProps={{
|
|
129
|
+
startAdornment: (
|
|
130
|
+
<InputAdornment position="start">
|
|
131
|
+
<SearchIcon sx={{ color: 'primary.main' }} />
|
|
132
|
+
</InputAdornment>
|
|
133
|
+
),
|
|
134
|
+
endAdornment: (
|
|
135
|
+
<InputAdornment position="end">
|
|
136
|
+
<Button
|
|
137
|
+
type="submit"
|
|
138
|
+
variant="contained"
|
|
139
|
+
disabled={loading || !query.trim()}
|
|
140
|
+
sx={{ minWidth: 100 }}
|
|
141
|
+
>
|
|
142
|
+
{loading ? <CircularProgress size={22} color="inherit" /> : 'Search'}
|
|
143
|
+
</Button>
|
|
144
|
+
</InputAdornment>
|
|
145
|
+
),
|
|
146
|
+
sx: { pr: 1 },
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
113
149
|
</Box>
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
150
|
+
|
|
151
|
+
{/* Error */}
|
|
152
|
+
{error && (
|
|
153
|
+
<Alert severity="error" sx={{ mb: 3, borderRadius: 2 }}>
|
|
154
|
+
{error}
|
|
155
|
+
</Alert>
|
|
156
|
+
)}
|
|
157
|
+
|
|
158
|
+
{/* Meta chips */}
|
|
159
|
+
{meta && (
|
|
160
|
+
<Stack direction="row" spacing={1} sx={{ mb: 3 }} flexWrap="wrap">
|
|
161
|
+
<Chip label={`${results.length} results`} size="small" color="primary" />
|
|
162
|
+
<Chip label={`${meta.took} ms`} size="small" variant="outlined" />
|
|
163
|
+
<Chip label={meta.model} size="small" variant="outlined" sx={{ fontFamily: 'monospace', fontSize: '0.7rem' }} />
|
|
164
|
+
{{#if rerank}}
|
|
165
|
+
<Chip label={`reranked: ${meta.rerankModel}`} size="small" color="success" sx={{ fontFamily: 'monospace', fontSize: '0.7rem' }} />
|
|
166
|
+
{{/if}}
|
|
167
|
+
</Stack>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{/* Results */}
|
|
171
|
+
<Stack spacing={2}>
|
|
172
|
+
{results.map((result, index) => (
|
|
173
|
+
<Fade in key={index} timeout={200 + index * 80}>
|
|
174
|
+
<Card>
|
|
175
|
+
<CardContent sx={{ p: 3, '&:last-child': { pb: 3 } }}>
|
|
176
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1.5 }}>
|
|
177
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
178
|
+
<Chip
|
|
179
|
+
label={`#${index + 1}`}
|
|
180
|
+
size="small"
|
|
181
|
+
sx={{
|
|
182
|
+
fontWeight: 700,
|
|
183
|
+
bgcolor: alpha(theme.palette.primary.main, 0.1),
|
|
184
|
+
color: 'primary.dark',
|
|
185
|
+
}}
|
|
186
|
+
/>
|
|
187
|
+
{result.metadata?.source && (
|
|
188
|
+
<Typography variant="caption" color="text.secondary" fontFamily="monospace">
|
|
189
|
+
{result.metadata.source}
|
|
190
|
+
</Typography>
|
|
191
|
+
)}
|
|
192
|
+
</Stack>
|
|
193
|
+
<ScoreBar score={result.score} />
|
|
194
|
+
</Stack>
|
|
195
|
+
|
|
196
|
+
<Typography
|
|
197
|
+
variant="body2"
|
|
198
|
+
sx={{
|
|
199
|
+
whiteSpace: 'pre-wrap',
|
|
200
|
+
lineHeight: 1.7,
|
|
201
|
+
color: 'text.primary',
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
{result.text}
|
|
124
205
|
</Typography>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
</
|
|
134
|
-
</
|
|
135
|
-
</
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
206
|
+
|
|
207
|
+
<Stack direction="row" justifyContent="flex-end" sx={{ mt: 1.5 }}>
|
|
208
|
+
<Tooltip title={copied === index ? 'Copied!' : 'Copy text'}>
|
|
209
|
+
<IconButton size="small" onClick={() => copyText(result.text, index)}>
|
|
210
|
+
<ContentCopyIcon fontSize="small" />
|
|
211
|
+
</IconButton>
|
|
212
|
+
</Tooltip>
|
|
213
|
+
</Stack>
|
|
214
|
+
</CardContent>
|
|
215
|
+
</Card>
|
|
216
|
+
</Fade>
|
|
217
|
+
))}
|
|
218
|
+
</Stack>
|
|
219
|
+
|
|
220
|
+
{/* Empty state */}
|
|
221
|
+
{results.length === 0 && !loading && query && !error && (
|
|
222
|
+
<Box sx={{ textAlign: 'center', py: 8 }}>
|
|
223
|
+
<Typography variant="h6" color="text.secondary" gutterBottom>
|
|
224
|
+
No results found
|
|
225
|
+
</Typography>
|
|
226
|
+
<Typography variant="body2" color="text.secondary">
|
|
227
|
+
Try rephrasing your query or ingest some documents first.
|
|
228
|
+
</Typography>
|
|
229
|
+
</Box>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* Initial state */}
|
|
233
|
+
{!meta && !loading && !error && (
|
|
234
|
+
<Box sx={{ textAlign: 'center', py: 8 }}>
|
|
235
|
+
<SearchIcon sx={{ fontSize: 48, color: 'text.disabled', mb: 2 }} />
|
|
236
|
+
<Typography variant="h6" color="text.secondary" gutterBottom>
|
|
237
|
+
Start searching
|
|
238
|
+
</Typography>
|
|
239
|
+
<Typography variant="body2" color="text.secondary">
|
|
240
|
+
Enter a natural-language query to find semantically similar documents.
|
|
241
|
+
</Typography>
|
|
242
|
+
</Box>
|
|
243
|
+
)}
|
|
244
|
+
</Container>
|
|
245
|
+
|
|
246
|
+
<Footer />
|
|
247
|
+
</Box>
|
|
146
248
|
);
|
|
147
249
|
}
|