voyageai-cli 1.24.0 → 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/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/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/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
|
@@ -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
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Registry — light/dark toggle with localStorage persistence
|
|
3
|
+
* Generated by vai v{{vaiVersion}} on {{generatedAt}}
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import { createContext, useContext, useMemo, useState, useEffect } from 'react';
|
|
9
|
+
import { ThemeProvider } from '@mui/material/styles';
|
|
10
|
+
import CssBaseline from '@mui/material/CssBaseline';
|
|
11
|
+
import { lightTheme, darkTheme } from '@/lib/theme';
|
|
12
|
+
|
|
13
|
+
const ColorModeContext = createContext({ toggle: () => {}, mode: 'light' });
|
|
14
|
+
|
|
15
|
+
export function useColorMode() {
|
|
16
|
+
return useContext(ColorModeContext);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ThemeRegistry({ children }) {
|
|
20
|
+
const [mode, setMode] = useState('light');
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const stored = localStorage.getItem('vai-color-mode');
|
|
24
|
+
if (stored === 'dark' || stored === 'light') {
|
|
25
|
+
setMode(stored);
|
|
26
|
+
} else if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
|
|
27
|
+
setMode('dark');
|
|
28
|
+
}
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const colorMode = useMemo(() => ({
|
|
32
|
+
mode,
|
|
33
|
+
toggle: () =>
|
|
34
|
+
setMode((prev) => {
|
|
35
|
+
const next = prev === 'light' ? 'dark' : 'light';
|
|
36
|
+
localStorage.setItem('vai-color-mode', next);
|
|
37
|
+
return next;
|
|
38
|
+
}),
|
|
39
|
+
}), [mode]);
|
|
40
|
+
|
|
41
|
+
const theme = mode === 'dark' ? darkTheme : lightTheme;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ColorModeContext.Provider value={colorMode}>
|
|
45
|
+
<ThemeProvider theme={theme}>
|
|
46
|
+
<CssBaseline />
|
|
47
|
+
{children}
|
|
48
|
+
</ThemeProvider>
|
|
49
|
+
</ColorModeContext.Provider>
|
|
50
|
+
);
|
|
51
|
+
}
|