@geminilight/mindos 0.3.0 → 0.5.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/app/app/api/mcp/agents/route.ts +72 -0
- package/app/app/api/mcp/install/route.ts +95 -0
- package/app/app/api/mcp/status/route.ts +47 -0
- package/app/app/api/setup/check-port/route.ts +41 -0
- package/app/app/api/skills/route.ts +208 -0
- package/app/app/api/sync/route.ts +54 -3
- package/app/app/api/update-check/route.ts +52 -0
- package/app/app/globals.css +12 -0
- package/app/app/layout.tsx +4 -2
- package/app/app/login/page.tsx +20 -13
- package/app/app/page.tsx +19 -2
- package/app/app/setup/page.tsx +2 -0
- package/app/app/view/[...path]/ViewPageClient.tsx +47 -21
- package/app/app/view/[...path]/loading.tsx +1 -1
- package/app/app/view/[...path]/not-found.tsx +101 -0
- package/app/components/AskFab.tsx +1 -1
- package/app/components/AskModal.tsx +1 -1
- package/app/components/Backlinks.tsx +1 -1
- package/app/components/Breadcrumb.tsx +13 -3
- package/app/components/CsvView.tsx +5 -6
- package/app/components/DirView.tsx +42 -21
- package/app/components/FindInPage.tsx +211 -0
- package/app/components/HomeContent.tsx +97 -44
- package/app/components/JsonView.tsx +1 -2
- package/app/components/MarkdownEditor.tsx +1 -2
- package/app/components/OnboardingView.tsx +6 -7
- package/app/components/SettingsModal.tsx +5 -2
- package/app/components/SetupWizard.tsx +499 -172
- package/app/components/Sidebar.tsx +1 -1
- package/app/components/UpdateBanner.tsx +101 -0
- package/app/components/renderers/{AgentInspectorRenderer.tsx → agent-inspector/AgentInspectorRenderer.tsx} +13 -11
- package/app/components/renderers/agent-inspector/manifest.ts +14 -0
- package/app/components/renderers/{BacklinksRenderer.tsx → backlinks/BacklinksRenderer.tsx} +6 -6
- package/app/components/renderers/backlinks/manifest.ts +14 -0
- package/app/components/renderers/config/manifest.ts +14 -0
- package/app/components/renderers/csv/BoardView.tsx +12 -12
- package/app/components/renderers/csv/ConfigPanel.tsx +7 -8
- package/app/components/renderers/{CsvRenderer.tsx → csv/CsvRenderer.tsx} +8 -9
- package/app/components/renderers/csv/GalleryView.tsx +3 -3
- package/app/components/renderers/csv/TableView.tsx +4 -5
- package/app/components/renderers/csv/manifest.ts +14 -0
- package/app/components/renderers/{DiffRenderer.tsx → diff/DiffRenderer.tsx} +10 -9
- package/app/components/renderers/diff/manifest.ts +14 -0
- package/app/components/renderers/{GraphRenderer.tsx → graph/GraphRenderer.tsx} +4 -5
- package/app/components/renderers/graph/manifest.ts +14 -0
- package/app/components/renderers/{SummaryRenderer.tsx → summary/SummaryRenderer.tsx} +6 -6
- package/app/components/renderers/summary/manifest.ts +14 -0
- package/app/components/renderers/{TimelineRenderer.tsx → timeline/TimelineRenderer.tsx} +6 -6
- package/app/components/renderers/timeline/manifest.ts +14 -0
- package/app/components/renderers/{TodoRenderer.tsx → todo/TodoRenderer.tsx} +2 -2
- package/app/components/renderers/todo/manifest.ts +14 -0
- package/app/components/renderers/{WorkflowRenderer.tsx → workflow/WorkflowRenderer.tsx} +13 -13
- package/app/components/renderers/workflow/manifest.ts +14 -0
- package/app/components/settings/McpTab.tsx +549 -0
- package/app/components/settings/SyncTab.tsx +139 -50
- package/app/components/settings/types.ts +1 -1
- package/app/data/pages/home.png +0 -0
- package/app/lib/i18n.ts +226 -19
- package/app/lib/renderers/index.ts +20 -89
- package/app/lib/renderers/registry.ts +4 -1
- package/app/lib/settings.ts +3 -0
- package/app/package.json +1 -0
- package/app/types/semver.d.ts +8 -0
- package/bin/cli.js +137 -24
- package/bin/lib/build.js +53 -18
- package/bin/lib/colors.js +3 -1
- package/bin/lib/config.js +4 -0
- package/bin/lib/constants.js +2 -0
- package/bin/lib/debug.js +10 -0
- package/bin/lib/mcp-install.js +4 -1
- package/bin/lib/port.js +8 -2
- package/bin/lib/startup.js +21 -20
- package/bin/lib/stop.js +41 -3
- package/bin/lib/sync.js +65 -53
- package/bin/lib/update-check.js +94 -0
- package/bin/lib/utils.js +2 -2
- package/package.json +1 -1
- package/scripts/gen-renderer-index.js +57 -0
- package/scripts/setup.js +205 -10
- /package/app/components/renderers/{ConfigRenderer.tsx → config/ConfigRenderer.tsx} +0 -0
package/app/app/login/page.tsx
CHANGED
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
import { useState, Suspense } from 'react';
|
|
4
4
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
5
5
|
import { Loader2, Lock } from 'lucide-react';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
6
7
|
|
|
7
8
|
function LoginForm() {
|
|
8
9
|
const router = useRouter();
|
|
9
10
|
const searchParams = useSearchParams();
|
|
11
|
+
const { t } = useLocale();
|
|
10
12
|
const [password, setPassword] = useState('');
|
|
11
13
|
const [loading, setLoading] = useState(false);
|
|
12
14
|
const [error, setError] = useState('');
|
|
13
15
|
|
|
16
|
+
const loginT = t.login;
|
|
17
|
+
|
|
14
18
|
async function handleSubmit(e: React.FormEvent) {
|
|
15
19
|
e.preventDefault();
|
|
16
20
|
setLoading(true);
|
|
@@ -29,11 +33,11 @@ function LoginForm() {
|
|
|
29
33
|
: '/';
|
|
30
34
|
router.replace(safe);
|
|
31
35
|
} else {
|
|
32
|
-
setError('Incorrect password. Please try again.');
|
|
36
|
+
setError(loginT?.incorrectPassword ?? 'Incorrect password. Please try again.');
|
|
33
37
|
setPassword('');
|
|
34
38
|
}
|
|
35
39
|
} catch {
|
|
36
|
-
setError('Connection error. Please try again.');
|
|
40
|
+
setError(loginT?.connectionError ?? 'Connection error. Please try again.');
|
|
37
41
|
} finally {
|
|
38
42
|
setLoading(false);
|
|
39
43
|
}
|
|
@@ -61,26 +65,28 @@ function LoginForm() {
|
|
|
61
65
|
<path d="M35,17.5 Q35,20 37.5,20 Q35,20 35,22.5 Q35,20 32.5,20 Q35,20 35,17.5 Z" fill="#FEF3C7"/>
|
|
62
66
|
</g>
|
|
63
67
|
</svg>
|
|
64
|
-
<h1
|
|
65
|
-
className="text-xl font-semibold text-foreground tracking-tight"
|
|
66
|
-
style={{ fontFamily: "'IBM Plex Mono', monospace" }}
|
|
67
|
-
>
|
|
68
|
+
<h1 className="text-xl font-semibold text-foreground tracking-tight font-display">
|
|
68
69
|
MindOS
|
|
69
70
|
</h1>
|
|
70
|
-
<p className="text-
|
|
71
|
+
<p className="text-xs text-muted-foreground/70 italic">
|
|
72
|
+
{loginT?.tagline ?? 'You think here, Agents act there.'}
|
|
73
|
+
</p>
|
|
74
|
+
<p className="text-sm text-muted-foreground">
|
|
75
|
+
{loginT?.subtitle ?? 'Enter your password to continue'}
|
|
76
|
+
</p>
|
|
71
77
|
</div>
|
|
72
78
|
|
|
73
79
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
74
80
|
<div className="space-y-1.5">
|
|
75
81
|
<label className="text-sm font-medium text-foreground" htmlFor="password">
|
|
76
|
-
Password
|
|
82
|
+
{loginT?.passwordLabel ?? 'Password'}
|
|
77
83
|
</label>
|
|
78
84
|
<input
|
|
79
85
|
id="password"
|
|
80
86
|
type="password"
|
|
81
87
|
value={password}
|
|
82
88
|
onChange={e => setPassword(e.target.value)}
|
|
83
|
-
placeholder=
|
|
89
|
+
placeholder={loginT?.passwordPlaceholder ?? 'Enter password'}
|
|
84
90
|
autoFocus
|
|
85
91
|
autoComplete="current-password"
|
|
86
92
|
required
|
|
@@ -89,21 +95,22 @@ function LoginForm() {
|
|
|
89
95
|
</div>
|
|
90
96
|
|
|
91
97
|
{error && (
|
|
92
|
-
<p className="text-xs text-destructive">{error}</p>
|
|
98
|
+
<p className="text-xs text-destructive" role="alert" aria-live="polite">{error}</p>
|
|
93
99
|
)}
|
|
94
100
|
|
|
95
101
|
<button
|
|
96
102
|
type="submit"
|
|
97
103
|
disabled={loading || !password}
|
|
98
|
-
className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed mt-2"
|
|
99
|
-
style={{ background: 'var(--amber)', color: '#131210' }}
|
|
104
|
+
className="w-full flex items-center justify-center gap-2 py-2 px-4 rounded-lg text-sm font-medium transition-opacity disabled:opacity-50 disabled:cursor-not-allowed mt-2 bg-[var(--amber)] text-[#131210]"
|
|
100
105
|
>
|
|
101
106
|
{loading ? (
|
|
102
107
|
<Loader2 size={14} className="animate-spin" />
|
|
103
108
|
) : (
|
|
104
109
|
<Lock size={14} />
|
|
105
110
|
)}
|
|
106
|
-
{loading
|
|
111
|
+
{loading
|
|
112
|
+
? (loginT?.signingIn ?? 'Signing in…')
|
|
113
|
+
: (loginT?.signIn ?? 'Sign in')}
|
|
107
114
|
</button>
|
|
108
115
|
</form>
|
|
109
116
|
</div>
|
package/app/app/page.tsx
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { redirect } from 'next/navigation';
|
|
2
2
|
import { readSettings } from '@/lib/settings';
|
|
3
|
-
import { getRecentlyModified } from '@/lib/fs';
|
|
3
|
+
import { getRecentlyModified, getFileContent } from '@/lib/fs';
|
|
4
|
+
import { getAllRenderers } from '@/lib/renderers/registry';
|
|
5
|
+
import '@/lib/renderers/index'; // registers all renderers
|
|
4
6
|
import HomeContent from '@/components/HomeContent';
|
|
5
7
|
|
|
8
|
+
export const dynamic = 'force-dynamic';
|
|
9
|
+
|
|
10
|
+
function getExistingFiles(paths: string[]): string[] {
|
|
11
|
+
return paths.filter(p => {
|
|
12
|
+
try { getFileContent(p); return true; } catch { return false; }
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
export default function HomePage() {
|
|
7
17
|
const settings = readSettings();
|
|
8
18
|
if (settings.setupPending) redirect('/setup');
|
|
@@ -13,5 +23,12 @@ export default function HomePage() {
|
|
|
13
23
|
} catch (err) {
|
|
14
24
|
console.error('[HomePage] Failed to load recent files:', err);
|
|
15
25
|
}
|
|
16
|
-
|
|
26
|
+
|
|
27
|
+
// Derive plugin entry paths from registry — no hardcoded list needed
|
|
28
|
+
const entryPaths = getAllRenderers()
|
|
29
|
+
.map(r => r.entryPath)
|
|
30
|
+
.filter((p): p is string => !!p);
|
|
31
|
+
const existingFiles = getExistingFiles(entryPaths);
|
|
32
|
+
|
|
33
|
+
return <HomeContent recent={recent} existingFiles={existingFiles} />;
|
|
17
34
|
}
|
package/app/app/setup/page.tsx
CHANGED
|
@@ -2,6 +2,8 @@ import { redirect } from 'next/navigation';
|
|
|
2
2
|
import { readSettings } from '@/lib/settings';
|
|
3
3
|
import SetupWizard from '@/components/SetupWizard';
|
|
4
4
|
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
5
7
|
export default function SetupPage() {
|
|
6
8
|
const settings = readSettings();
|
|
7
9
|
if (!settings.setupPending) redirect('/');
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useTransition, useCallback, useEffect, useSyncExternalStore } from 'react';
|
|
3
|
+
import { useState, useTransition, useCallback, useEffect, useRef, useSyncExternalStore, useMemo, Suspense } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
|
-
import { Edit3, Save, X, Loader2, LayoutTemplate } from 'lucide-react';
|
|
5
|
+
import { Edit3, Save, X, Loader2, LayoutTemplate, ArrowLeft } from 'lucide-react';
|
|
6
|
+
import { lazy } from 'react';
|
|
6
7
|
import MarkdownView from '@/components/MarkdownView';
|
|
7
8
|
import JsonView from '@/components/JsonView';
|
|
8
9
|
import CsvView from '@/components/CsvView';
|
|
@@ -10,6 +11,7 @@ import Backlinks from '@/components/Backlinks';
|
|
|
10
11
|
import Breadcrumb from '@/components/Breadcrumb';
|
|
11
12
|
import MarkdownEditor, { MdViewMode } from '@/components/MarkdownEditor';
|
|
12
13
|
import TableOfContents from '@/components/TableOfContents';
|
|
14
|
+
import FindInPage from '@/components/FindInPage';
|
|
13
15
|
import { resolveRenderer } from '@/lib/renderers/registry';
|
|
14
16
|
import { encodePath } from '@/lib/utils';
|
|
15
17
|
import '@/lib/renderers/index'; // registers all renderers
|
|
@@ -67,6 +69,8 @@ export default function ViewPageClient({
|
|
|
67
69
|
const [saveError, setSaveError] = useState<string | null>(null);
|
|
68
70
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
69
71
|
const [mdViewMode, setMdViewMode] = useState<MdViewMode>('wysiwyg');
|
|
72
|
+
const [findOpen, setFindOpen] = useState(false);
|
|
73
|
+
const contentRef = useRef<HTMLDivElement>(null);
|
|
70
74
|
|
|
71
75
|
const inferredName = filePath.split('/').pop() || 'Untitled.md';
|
|
72
76
|
const [showSaveAs, setShowSaveAs] = useState(isDraft);
|
|
@@ -86,6 +90,14 @@ export default function ViewPageClient({
|
|
|
86
90
|
const isCsv = extension === 'csv';
|
|
87
91
|
const showRenderer = !editing && !effectiveUseRaw && !!renderer;
|
|
88
92
|
|
|
93
|
+
// Lazily resolve the renderer component for code-splitting
|
|
94
|
+
const LazyComponent = useMemo(() => {
|
|
95
|
+
if (!renderer) return null;
|
|
96
|
+
if (renderer.component) return renderer.component;
|
|
97
|
+
if (renderer.load) return lazy(renderer.load);
|
|
98
|
+
return null;
|
|
99
|
+
}, [renderer]);
|
|
100
|
+
|
|
89
101
|
const handleEdit = useCallback(() => {
|
|
90
102
|
setEditContent(savedContent);
|
|
91
103
|
setEditing(true);
|
|
@@ -172,6 +184,10 @@ export default function ViewPageClient({
|
|
|
172
184
|
e.preventDefault();
|
|
173
185
|
if (editing) handleSave();
|
|
174
186
|
}
|
|
187
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'f' && !editing) {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
setFindOpen(true);
|
|
190
|
+
}
|
|
175
191
|
if (e.key === 'e' && !editing && document.activeElement?.tagName === 'BODY') {
|
|
176
192
|
handleEdit();
|
|
177
193
|
}
|
|
@@ -186,13 +202,20 @@ export default function ViewPageClient({
|
|
|
186
202
|
{/* Top bar */}
|
|
187
203
|
<div className="sticky top-[52px] md:top-0 z-20 border-b border-border px-4 md:px-6 py-2.5" style={{ background: 'var(--background)' }}>
|
|
188
204
|
<div className="content-width xl:mr-[220px] flex items-center justify-between gap-2">
|
|
189
|
-
<div className="min-w-0 flex-1">
|
|
205
|
+
<div className="min-w-0 flex-1 flex items-center gap-1.5">
|
|
206
|
+
<button
|
|
207
|
+
onClick={() => router.back()}
|
|
208
|
+
className="md:hidden p-1 -ml-1 rounded text-muted-foreground hover:text-foreground transition-colors shrink-0"
|
|
209
|
+
aria-label="Go back"
|
|
210
|
+
>
|
|
211
|
+
<ArrowLeft size={16} />
|
|
212
|
+
</button>
|
|
190
213
|
<Breadcrumb filePath={filePath} />
|
|
191
214
|
</div>
|
|
192
215
|
|
|
193
216
|
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
|
|
194
217
|
{saveSuccess && (
|
|
195
|
-
<span className="text-xs flex items-center gap-1.5" style={{ color: '#7aad80'
|
|
218
|
+
<span className="text-xs flex items-center gap-1.5 font-display" style={{ color: '#7aad80' }}>
|
|
196
219
|
<span className="w-1.5 h-1.5 rounded-full" style={{ background: '#7aad80' }} />
|
|
197
220
|
<span className="hidden sm:inline">saved</span>
|
|
198
221
|
</span>
|
|
@@ -205,11 +228,10 @@ export default function ViewPageClient({
|
|
|
205
228
|
{renderer && !editing && !isDraft && (
|
|
206
229
|
<button
|
|
207
230
|
onClick={handleToggleRaw}
|
|
208
|
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
|
231
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors font-display"
|
|
209
232
|
style={{
|
|
210
233
|
background: effectiveUseRaw ? 'var(--muted)' : `${'var(--amber)'}22`,
|
|
211
234
|
color: effectiveUseRaw ? 'var(--muted-foreground)' : 'var(--amber)',
|
|
212
|
-
fontFamily: "'IBM Plex Mono', monospace",
|
|
213
235
|
}}
|
|
214
236
|
title={effectiveUseRaw ? `Switch to ${renderer.name}` : 'View raw'}
|
|
215
237
|
>
|
|
@@ -221,8 +243,8 @@ export default function ViewPageClient({
|
|
|
221
243
|
{!editing && !showRenderer && !isDraft && (
|
|
222
244
|
<button
|
|
223
245
|
onClick={handleEdit}
|
|
224
|
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors"
|
|
225
|
-
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)'
|
|
246
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors font-display"
|
|
247
|
+
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}
|
|
226
248
|
onMouseEnter={(e) => { e.currentTarget.style.color = 'var(--foreground)'; e.currentTarget.style.background = 'var(--accent)'; }}
|
|
227
249
|
onMouseLeave={(e) => { e.currentTarget.style.color = 'var(--muted-foreground)'; e.currentTarget.style.background = 'var(--muted)'; }}
|
|
228
250
|
>
|
|
@@ -235,8 +257,8 @@ export default function ViewPageClient({
|
|
|
235
257
|
<button
|
|
236
258
|
onClick={handleCancel}
|
|
237
259
|
disabled={isPending}
|
|
238
|
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors disabled:opacity-50"
|
|
239
|
-
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)'
|
|
260
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors disabled:opacity-50 font-display"
|
|
261
|
+
style={{ background: 'var(--muted)', color: 'var(--muted-foreground)' }}
|
|
240
262
|
onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--accent)'; }}
|
|
241
263
|
onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--muted)'; }}
|
|
242
264
|
>
|
|
@@ -246,8 +268,8 @@ export default function ViewPageClient({
|
|
|
246
268
|
<button
|
|
247
269
|
onClick={isDraft && showSaveAs ? handleConfirmDraftSave : handleSave}
|
|
248
270
|
disabled={isPending}
|
|
249
|
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium disabled:opacity-50"
|
|
250
|
-
style={{ background: 'var(--amber)', color: '#131210'
|
|
271
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium disabled:opacity-50 font-display"
|
|
272
|
+
style={{ background: 'var(--amber)', color: '#131210' }}
|
|
251
273
|
>
|
|
252
274
|
{isPending ? <Loader2 size={13} className="animate-spin" /> : <Save size={13} />}
|
|
253
275
|
<span className="hidden sm:inline">Save</span>
|
|
@@ -309,18 +331,22 @@ export default function ViewPageClient({
|
|
|
309
331
|
/>
|
|
310
332
|
)}
|
|
311
333
|
</div>
|
|
312
|
-
) : showRenderer ? (
|
|
313
|
-
<div className="content-width xl:mr-[220px]">
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
334
|
+
) : showRenderer && LazyComponent ? (
|
|
335
|
+
<div ref={contentRef} className="content-width xl:mr-[220px]">
|
|
336
|
+
{findOpen && <FindInPage containerRef={contentRef} onClose={() => setFindOpen(false)} />}
|
|
337
|
+
<Suspense fallback={<div className="flex items-center justify-center py-12"><Loader2 size={20} className="animate-spin text-muted-foreground" /></div>}>
|
|
338
|
+
<LazyComponent
|
|
339
|
+
filePath={filePath}
|
|
340
|
+
content={savedContent}
|
|
341
|
+
extension={extension}
|
|
342
|
+
saveAction={handleRendererSave}
|
|
343
|
+
/>
|
|
344
|
+
</Suspense>
|
|
320
345
|
<Backlinks filePath={filePath} />
|
|
321
346
|
</div>
|
|
322
347
|
) : (
|
|
323
|
-
<div className="content-width xl:mr-[220px]">
|
|
348
|
+
<div ref={contentRef} className="content-width xl:mr-[220px]">
|
|
349
|
+
{findOpen && <FindInPage containerRef={contentRef} onClose={() => setFindOpen(false)} />}
|
|
324
350
|
{extension === 'csv' ? (
|
|
325
351
|
<CsvView
|
|
326
352
|
content={savedContent}
|
|
@@ -6,7 +6,7 @@ export default function Loading() {
|
|
|
6
6
|
className="w-6 h-6 border-2 rounded-full animate-spin"
|
|
7
7
|
style={{ borderColor: 'var(--border)', borderTopColor: 'var(--amber)' }}
|
|
8
8
|
/>
|
|
9
|
-
<span className="text-xs text-muted-foreground
|
|
9
|
+
<span className="text-xs text-muted-foreground font-display">
|
|
10
10
|
Loading...
|
|
11
11
|
</span>
|
|
12
12
|
</div>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { usePathname, useRouter } from 'next/navigation';
|
|
4
|
+
import { FilePlus, Home, ArrowLeft } from 'lucide-react';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
|
+
import { encodePath } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
export default function ViewNotFound() {
|
|
10
|
+
const pathname = usePathname();
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
const { t } = useLocale();
|
|
13
|
+
const [creating, setCreating] = useState(false);
|
|
14
|
+
|
|
15
|
+
// Extract the attempted file path from /view/...
|
|
16
|
+
const filePath = pathname.startsWith('/view/')
|
|
17
|
+
? decodeURIComponent(pathname.slice('/view/'.length))
|
|
18
|
+
: '';
|
|
19
|
+
|
|
20
|
+
const parentDir = filePath.includes('/')
|
|
21
|
+
? filePath.split('/').slice(0, -1).join('/')
|
|
22
|
+
: '';
|
|
23
|
+
|
|
24
|
+
const isMd = filePath.endsWith('.md') || !filePath.includes('.');
|
|
25
|
+
const displayPath = filePath || 'unknown';
|
|
26
|
+
|
|
27
|
+
const notFoundT = t.notFound;
|
|
28
|
+
|
|
29
|
+
const handleCreate = async () => {
|
|
30
|
+
setCreating(true);
|
|
31
|
+
try {
|
|
32
|
+
const target = isMd && !filePath.includes('.') ? `${filePath}.md` : filePath;
|
|
33
|
+
const res = await fetch('/api/files', {
|
|
34
|
+
method: 'PUT',
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ path: target, content: '' }),
|
|
37
|
+
});
|
|
38
|
+
if (res.ok) {
|
|
39
|
+
router.replace(`/view/${encodePath(target)}`);
|
|
40
|
+
router.refresh();
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// silent — user can retry
|
|
44
|
+
} finally {
|
|
45
|
+
setCreating(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="content-width px-4 md:px-6 py-16 md:py-24 flex flex-col items-center text-center">
|
|
51
|
+
<div className="w-12 h-12 rounded-xl bg-muted flex items-center justify-center mb-6">
|
|
52
|
+
<FilePlus size={24} className="text-muted-foreground" />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<h1 className="text-lg font-semibold text-foreground mb-2 font-display">
|
|
56
|
+
{notFoundT?.title ?? 'File not found'}
|
|
57
|
+
</h1>
|
|
58
|
+
|
|
59
|
+
<p className="text-sm text-muted-foreground mb-1">
|
|
60
|
+
<code className="px-1.5 py-0.5 rounded bg-muted font-mono text-xs">{displayPath}</code>
|
|
61
|
+
</p>
|
|
62
|
+
<p className="text-sm text-muted-foreground mb-8">
|
|
63
|
+
{notFoundT?.description ?? 'This file does not exist in your knowledge base.'}
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
<div className="flex flex-wrap items-center justify-center gap-3">
|
|
67
|
+
{isMd && (
|
|
68
|
+
<button
|
|
69
|
+
onClick={handleCreate}
|
|
70
|
+
disabled={creating}
|
|
71
|
+
className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
|
72
|
+
style={{ background: 'var(--amber)', color: '#131210' }}
|
|
73
|
+
>
|
|
74
|
+
<FilePlus size={14} />
|
|
75
|
+
{creating
|
|
76
|
+
? (notFoundT?.creating ?? 'Creating...')
|
|
77
|
+
: (notFoundT?.createButton ?? 'Create this file')}
|
|
78
|
+
</button>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{parentDir && (
|
|
82
|
+
<button
|
|
83
|
+
onClick={() => router.push(`/view/${encodePath(parentDir)}`)}
|
|
84
|
+
className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
85
|
+
>
|
|
86
|
+
<ArrowLeft size={14} />
|
|
87
|
+
{notFoundT?.goToParent ?? 'Go to parent folder'}
|
|
88
|
+
</button>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
<button
|
|
92
|
+
onClick={() => router.push('/')}
|
|
93
|
+
className="flex items-center gap-2 px-4 py-2.5 text-sm font-medium rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
94
|
+
>
|
|
95
|
+
<Home size={14} />
|
|
96
|
+
{notFoundT?.goHome ?? 'Home'}
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -31,9 +31,9 @@ export default function AskFab() {
|
|
|
31
31
|
active:scale-95
|
|
32
32
|
cursor-pointer
|
|
33
33
|
overflow-hidden
|
|
34
|
+
font-display
|
|
34
35
|
"
|
|
35
36
|
style={{
|
|
36
|
-
fontFamily: "'IBM Plex Mono', monospace",
|
|
37
37
|
background: 'linear-gradient(135deg, #b07c2e 0%, #c8873a 50%, #d4943f 100%)',
|
|
38
38
|
marginBottom: 'env(safe-area-inset-bottom, 0px)',
|
|
39
39
|
}}
|
|
@@ -244,7 +244,7 @@ export default function AskModal({ open, onClose, currentFile }: AskModalProps)
|
|
|
244
244
|
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-8 h-1 rounded-full bg-muted-foreground/20 md:hidden" />
|
|
245
245
|
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
246
246
|
<Sparkles size={15} style={{ color: 'var(--amber)' }} />
|
|
247
|
-
<span
|
|
247
|
+
<span className="font-display">{t.ask.title}</span>
|
|
248
248
|
{currentFile && (
|
|
249
249
|
<span className="text-xs text-muted-foreground font-normal truncate max-w-[200px]">
|
|
250
250
|
— {currentFile.split('/').pop()}
|
|
@@ -31,7 +31,7 @@ export default function Backlinks({ filePath }: { filePath: string }) {
|
|
|
31
31
|
<div className="mt-12 pt-8 border-t border-border">
|
|
32
32
|
<div className="flex items-center gap-2 mb-6 text-muted-foreground">
|
|
33
33
|
<LinkIcon size={16} className="text-amber-500/70" />
|
|
34
|
-
<h3 className="text-sm font-semibold tracking-wider uppercase
|
|
34
|
+
<h3 className="text-sm font-semibold tracking-wider uppercase font-display">
|
|
35
35
|
{t.common?.relatedFiles || 'Related Files'}
|
|
36
36
|
</h3>
|
|
37
37
|
<span className="text-xs bg-muted px-1.5 py-0.5 rounded-full font-mono">
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import Link from 'next/link';
|
|
4
|
-
import { ChevronRight, Home } from 'lucide-react';
|
|
4
|
+
import { ChevronRight, Home, FileText, Table, Folder } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
function FileTypeIcon({ name }: { name: string }) {
|
|
7
|
+
const ext = name.includes('.') ? name.slice(name.lastIndexOf('.')).toLowerCase() : '';
|
|
8
|
+
if (ext === '.csv') return <Table size={13} className="text-emerald-400 shrink-0" />;
|
|
9
|
+
if (ext) return <FileText size={13} className="text-zinc-400 shrink-0" />;
|
|
10
|
+
return <Folder size={13} className="text-yellow-400 shrink-0" />;
|
|
11
|
+
}
|
|
5
12
|
|
|
6
13
|
export default function Breadcrumb({ filePath }: { filePath: string }) {
|
|
7
14
|
const parts = filePath.split('/');
|
|
8
15
|
return (
|
|
9
|
-
<nav className="flex items-center gap-1 text-xs text-muted-foreground flex-wrap
|
|
16
|
+
<nav className="flex items-center gap-1 text-xs text-muted-foreground flex-wrap font-display">
|
|
10
17
|
<Link href="/" className="hover:text-foreground transition-colors">
|
|
11
18
|
<Home size={14} />
|
|
12
19
|
</Link>
|
|
@@ -17,7 +24,10 @@ export default function Breadcrumb({ filePath }: { filePath: string }) {
|
|
|
17
24
|
<span key={i} className="flex items-center gap-1">
|
|
18
25
|
<ChevronRight size={12} className="text-muted-foreground/50" />
|
|
19
26
|
{isLast ? (
|
|
20
|
-
<span className="text-foreground font-medium" suppressHydrationWarning>
|
|
27
|
+
<span className="flex items-center gap-1.5 text-foreground font-medium" suppressHydrationWarning>
|
|
28
|
+
<FileTypeIcon name={part} />
|
|
29
|
+
{part}
|
|
30
|
+
</span>
|
|
21
31
|
) : (
|
|
22
32
|
<Link href={href} className="hover:text-foreground transition-colors truncate max-w-[200px]" suppressHydrationWarning>
|
|
23
33
|
{part}
|
|
@@ -218,7 +218,6 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
|
|
|
218
218
|
style={{
|
|
219
219
|
color: 'var(--foreground)',
|
|
220
220
|
borderBottom: '1px solid var(--border)',
|
|
221
|
-
fontFamily: "'IBM Plex Sans', sans-serif",
|
|
222
221
|
fontSize: '0.75rem',
|
|
223
222
|
letterSpacing: '0.04em',
|
|
224
223
|
textTransform: 'uppercase',
|
|
@@ -296,15 +295,15 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
|
|
|
296
295
|
className="px-4 py-2 flex items-center justify-between"
|
|
297
296
|
style={{ background: 'var(--muted)', borderTop: '1px solid var(--border)' }}
|
|
298
297
|
>
|
|
299
|
-
<span className="text-xs" style={{ color: 'var(--muted-foreground)'
|
|
298
|
+
<span className="text-xs font-display" style={{ color: 'var(--muted-foreground)' }}>
|
|
300
299
|
{rows.length} rows · {headers.length} cols
|
|
301
300
|
</span>
|
|
302
301
|
|
|
303
302
|
{canEdit && !showAdd && (
|
|
304
303
|
<button
|
|
305
304
|
onClick={() => setShowAdd(true)}
|
|
306
|
-
className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md transition-colors"
|
|
307
|
-
style={{ color: 'var(--amber)', background: 'var(--amber-dim)'
|
|
305
|
+
className="flex items-center gap-1 text-xs px-2.5 py-1 rounded-md transition-colors font-display"
|
|
306
|
+
style={{ color: 'var(--amber)', background: 'var(--amber-dim)' }}
|
|
308
307
|
>
|
|
309
308
|
<Plus size={12} />
|
|
310
309
|
Add row
|
|
@@ -313,8 +312,8 @@ export default function CsvView({ content: initialContent, appendAction, saveAct
|
|
|
313
312
|
{showAdd && (
|
|
314
313
|
<button
|
|
315
314
|
onClick={() => setShowAdd(false)}
|
|
316
|
-
className="text-xs px-2.5 py-1 rounded-md transition-colors"
|
|
317
|
-
style={{ color: 'var(--muted-foreground)'
|
|
315
|
+
className="text-xs px-2.5 py-1 rounded-md transition-colors font-display"
|
|
316
|
+
style={{ color: 'var(--muted-foreground)' }}
|
|
318
317
|
>
|
|
319
318
|
Cancel
|
|
320
319
|
</button>
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useSyncExternalStore, useMemo } from 'react';
|
|
4
4
|
import Link from 'next/link';
|
|
5
|
-
import { FileText, Table, Folder, FolderOpen, LayoutGrid, List } from 'lucide-react';
|
|
5
|
+
import { FileText, Table, Folder, FolderOpen, LayoutGrid, List, FilePlus } from 'lucide-react';
|
|
6
6
|
import Breadcrumb from '@/components/Breadcrumb';
|
|
7
|
-
import { encodePath } from '@/lib/utils';
|
|
7
|
+
import { encodePath, relativeTime } from '@/lib/utils';
|
|
8
8
|
import { FileNode } from '@/lib/types';
|
|
9
9
|
import { useLocale } from '@/lib/LocaleContext';
|
|
10
10
|
|
|
@@ -57,6 +57,7 @@ function useDirViewPref() {
|
|
|
57
57
|
export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
58
58
|
const [view, setView] = useDirViewPref();
|
|
59
59
|
const { t } = useLocale();
|
|
60
|
+
const formatTime = (mtime: number) => relativeTime(mtime, t.home.relativeTime);
|
|
60
61
|
const fileCounts = useMemo(() => {
|
|
61
62
|
const map = new Map<string, number>();
|
|
62
63
|
for (const e of entries) map.set(e.path, countFiles(e));
|
|
@@ -71,22 +72,31 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
|
71
72
|
<div className="min-w-0 flex-1">
|
|
72
73
|
<Breadcrumb filePath={dirPath} />
|
|
73
74
|
</div>
|
|
74
|
-
{/* View toggle */}
|
|
75
|
-
<div className="flex items-center gap-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
className=
|
|
79
|
-
title={t.dirView.gridView}
|
|
75
|
+
{/* New file + View toggle */}
|
|
76
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
77
|
+
<Link
|
|
78
|
+
href={`/view/${encodePath(dirPath ? `${dirPath}/Untitled.md` : 'Untitled.md')}`}
|
|
79
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs rounded-lg transition-colors text-muted-foreground hover:text-foreground hover:bg-muted"
|
|
80
80
|
>
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
<FilePlus size={13} />
|
|
82
|
+
<span className="hidden sm:inline">{t.dirView.newFile}</span>
|
|
83
|
+
</Link>
|
|
84
|
+
<div className="flex items-center gap-1 p-1 bg-muted rounded-lg">
|
|
85
|
+
<button
|
|
86
|
+
onClick={() => setView('grid')}
|
|
87
|
+
className={`p-1.5 rounded transition-colors ${view === 'grid' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
88
|
+
title={t.dirView.gridView}
|
|
89
|
+
>
|
|
90
|
+
<LayoutGrid size={14} />
|
|
91
|
+
</button>
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => setView('list')}
|
|
94
|
+
className={`p-1.5 rounded transition-colors ${view === 'list' ? 'bg-card text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}`}
|
|
95
|
+
title={t.dirView.listView}
|
|
96
|
+
>
|
|
97
|
+
<List size={14} />
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
90
100
|
</div>
|
|
91
101
|
</div>
|
|
92
102
|
</div>
|
|
@@ -102,15 +112,26 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
|
102
112
|
<Link
|
|
103
113
|
key={entry.path}
|
|
104
114
|
href={`/view/${encodePath(entry.path)}`}
|
|
105
|
-
className=
|
|
115
|
+
className={
|
|
116
|
+
entry.type === 'directory'
|
|
117
|
+
? 'flex flex-col items-center gap-1.5 p-3 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center'
|
|
118
|
+
: 'flex flex-col items-center gap-2 p-4 rounded-xl border border-border bg-card hover:bg-accent hover:border-border/80 transition-all duration-100 text-center'
|
|
119
|
+
}
|
|
106
120
|
>
|
|
107
|
-
|
|
121
|
+
{entry.type === 'directory'
|
|
122
|
+
? <FolderOpen size={22} className="text-yellow-400" />
|
|
123
|
+
: <FileIconLarge node={entry} />}
|
|
108
124
|
<span className="text-xs text-foreground leading-snug line-clamp-2 w-full" suppressHydrationWarning>
|
|
109
125
|
{entry.name}
|
|
110
126
|
</span>
|
|
111
127
|
{entry.type === 'directory' && (
|
|
112
128
|
<span className="text-[10px] text-muted-foreground">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
|
|
113
129
|
)}
|
|
130
|
+
{entry.type === 'file' && entry.mtime && (
|
|
131
|
+
<span className="text-[10px] text-muted-foreground font-display" suppressHydrationWarning>
|
|
132
|
+
{formatTime(entry.mtime)}
|
|
133
|
+
</span>
|
|
134
|
+
)}
|
|
114
135
|
</Link>
|
|
115
136
|
))}
|
|
116
137
|
</div>
|
|
@@ -129,8 +150,8 @@ export default function DirView({ dirPath, entries }: DirViewProps) {
|
|
|
129
150
|
{entry.type === 'directory' ? (
|
|
130
151
|
<span className="text-xs text-muted-foreground shrink-0">{t.dirView.fileCount(fileCounts.get(entry.path) ?? 0)}</span>
|
|
131
152
|
) : entry.mtime ? (
|
|
132
|
-
<span className="text-xs text-muted-foreground shrink-0 tabular-nums
|
|
133
|
-
{
|
|
153
|
+
<span className="text-xs text-muted-foreground shrink-0 tabular-nums font-display" suppressHydrationWarning>
|
|
154
|
+
{formatTime(entry.mtime)}
|
|
134
155
|
</span>
|
|
135
156
|
) : null}
|
|
136
157
|
</Link>
|