@geminilight/mindos 0.5.41 → 0.5.43
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/icon.svg +25 -23
- package/app/app/layout.tsx +10 -4
- package/app/app/page.tsx +20 -1
- package/app/app/view/[...path]/ViewPageClient.tsx +17 -16
- package/app/components/ActivityBar.tsx +29 -3
- package/app/components/CreateSpaceModal.tsx +182 -0
- package/app/components/DirPicker.tsx +129 -0
- package/app/components/HomeContent.tsx +110 -226
- package/app/components/Panel.tsx +1 -0
- package/app/components/SidebarLayout.tsx +4 -0
- package/app/components/ThemeToggle.tsx +2 -2
- package/app/components/UpdateBanner.tsx +22 -30
- package/app/components/panels/DiscoverPanel.tsx +172 -0
- package/app/components/settings/AppearanceTab.tsx +126 -66
- package/app/components/settings/SettingsContent.tsx +23 -2
- package/app/lib/LocaleContext.tsx +16 -4
- package/app/lib/actions.ts +16 -5
- package/app/lib/i18n-en.ts +29 -2
- package/app/lib/i18n-zh.ts +29 -2
- package/app/package.json +2 -1
- package/bin/cli.js +12 -0
- package/bin/lib/skill-check.js +133 -0
- package/bin/lib/startup.js +4 -0
- package/package.json +1 -1
- package/scripts/fix-postcss-deps.cjs +30 -0
- package/skills/mindos/SKILL.md +78 -4
- package/skills/mindos-zh/SKILL.md +78 -4
- package/templates/en//360/237/223/235 Notes/Drafts/README.md" +8 -0
- package/templates/en//360/237/223/235 Notes/README.md" +2 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260/README.md" +2 -0
- package/templates/zh//360/237/223/235 /347/254/224/350/256/260//350/215/211/347/250/277/README.md" +8 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { ChevronDown, ChevronRight, ExternalLink, Blocks, Zap } from 'lucide-react';
|
|
6
|
+
import PanelHeader from './PanelHeader';
|
|
7
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
8
|
+
import { useCases } from '@/components/explore/use-cases';
|
|
9
|
+
import { openAskModal } from '@/hooks/useAskModal';
|
|
10
|
+
|
|
11
|
+
interface DiscoverPanelProps {
|
|
12
|
+
active: boolean;
|
|
13
|
+
maximized?: boolean;
|
|
14
|
+
onMaximize?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Collapsible section with count badge */
|
|
18
|
+
function Section({
|
|
19
|
+
icon,
|
|
20
|
+
title,
|
|
21
|
+
count,
|
|
22
|
+
defaultOpen = true,
|
|
23
|
+
children,
|
|
24
|
+
}: {
|
|
25
|
+
icon: React.ReactNode;
|
|
26
|
+
title: string;
|
|
27
|
+
count?: number;
|
|
28
|
+
defaultOpen?: boolean;
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
}) {
|
|
31
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
32
|
+
return (
|
|
33
|
+
<div>
|
|
34
|
+
<button
|
|
35
|
+
onClick={() => setOpen(v => !v)}
|
|
36
|
+
className="flex items-center gap-2 w-full px-4 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider hover:text-foreground transition-colors"
|
|
37
|
+
>
|
|
38
|
+
{open ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
39
|
+
<span className="flex items-center gap-1.5">
|
|
40
|
+
{icon}
|
|
41
|
+
{title}
|
|
42
|
+
</span>
|
|
43
|
+
{count !== undefined && (
|
|
44
|
+
<span className="ml-auto text-2xs tabular-nums opacity-60">{count}</span>
|
|
45
|
+
)}
|
|
46
|
+
</button>
|
|
47
|
+
{open && <div className="pb-2">{children}</div>}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Compact use case row for panel display */
|
|
53
|
+
function UseCaseRow({
|
|
54
|
+
icon,
|
|
55
|
+
title,
|
|
56
|
+
prompt,
|
|
57
|
+
tryLabel,
|
|
58
|
+
}: {
|
|
59
|
+
icon: string;
|
|
60
|
+
title: string;
|
|
61
|
+
prompt: string;
|
|
62
|
+
tryLabel: string;
|
|
63
|
+
}) {
|
|
64
|
+
return (
|
|
65
|
+
<div className="group flex items-center gap-2 px-4 py-1.5 hover:bg-muted/50 transition-colors rounded-sm mx-1">
|
|
66
|
+
<span className="text-sm leading-none shrink-0" suppressHydrationWarning>{icon}</span>
|
|
67
|
+
<span className="text-xs text-foreground truncate flex-1">{title}</span>
|
|
68
|
+
<button
|
|
69
|
+
onClick={() => openAskModal(prompt, 'user')}
|
|
70
|
+
className="opacity-0 group-hover:opacity-100 text-2xs px-2 py-0.5 rounded text-[var(--amber)] bg-[var(--amber-dim)] hover:opacity-80 transition-all duration-150 shrink-0"
|
|
71
|
+
>
|
|
72
|
+
{tryLabel}
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Coming soon placeholder */
|
|
79
|
+
function ComingSoonSection({
|
|
80
|
+
icon,
|
|
81
|
+
title,
|
|
82
|
+
description,
|
|
83
|
+
comingSoonLabel,
|
|
84
|
+
}: {
|
|
85
|
+
icon: React.ReactNode;
|
|
86
|
+
title: string;
|
|
87
|
+
description: string;
|
|
88
|
+
comingSoonLabel: string;
|
|
89
|
+
}) {
|
|
90
|
+
return (
|
|
91
|
+
<Section icon={icon} title={title} defaultOpen={false}>
|
|
92
|
+
<div className="px-4 py-3 mx-1">
|
|
93
|
+
<p className="text-xs text-muted-foreground leading-relaxed">{description}</p>
|
|
94
|
+
<span className="inline-block mt-2 text-2xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
|
|
95
|
+
{comingSoonLabel}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
</Section>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default function DiscoverPanel({ active, maximized, onMaximize }: DiscoverPanelProps) {
|
|
103
|
+
const { t } = useLocale();
|
|
104
|
+
const d = t.panels.discover;
|
|
105
|
+
const e = t.explore;
|
|
106
|
+
|
|
107
|
+
/** Type-safe lookup for use case i18n */
|
|
108
|
+
const getUseCaseText = (id: string): { title: string; prompt: string } | undefined => {
|
|
109
|
+
const map: Record<string, { title: string; desc: string; prompt: string }> = {
|
|
110
|
+
c1: e.c1, c2: e.c2, c3: e.c3, c4: e.c4, c5: e.c5,
|
|
111
|
+
c6: e.c6, c7: e.c7, c8: e.c8, c9: e.c9,
|
|
112
|
+
};
|
|
113
|
+
return map[id];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
118
|
+
<PanelHeader title={d.title} maximized={maximized} onMaximize={onMaximize} />
|
|
119
|
+
<div className="flex-1 overflow-y-auto min-h-0">
|
|
120
|
+
{/* Use Cases */}
|
|
121
|
+
<Section icon={<span className="text-xs" suppressHydrationWarning>🎯</span>} title={d.useCases} count={useCases.length}>
|
|
122
|
+
<div className="flex flex-col">
|
|
123
|
+
{useCases.map(uc => {
|
|
124
|
+
const data = getUseCaseText(uc.id);
|
|
125
|
+
if (!data) return null;
|
|
126
|
+
return (
|
|
127
|
+
<UseCaseRow
|
|
128
|
+
key={uc.id}
|
|
129
|
+
icon={uc.icon}
|
|
130
|
+
title={data.title}
|
|
131
|
+
prompt={data.prompt}
|
|
132
|
+
tryLabel={d.tryIt}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
</div>
|
|
137
|
+
</Section>
|
|
138
|
+
|
|
139
|
+
<div className="mx-4 border-t border-border" />
|
|
140
|
+
|
|
141
|
+
{/* Plugin Market — Coming Soon */}
|
|
142
|
+
<ComingSoonSection
|
|
143
|
+
icon={<Blocks size={11} />}
|
|
144
|
+
title={d.pluginMarket}
|
|
145
|
+
description={d.pluginMarketDesc}
|
|
146
|
+
comingSoonLabel={d.comingSoon}
|
|
147
|
+
/>
|
|
148
|
+
|
|
149
|
+
<div className="mx-4 border-t border-border" />
|
|
150
|
+
|
|
151
|
+
{/* Skill Market — Coming Soon */}
|
|
152
|
+
<ComingSoonSection
|
|
153
|
+
icon={<Zap size={11} />}
|
|
154
|
+
title={d.skillMarket}
|
|
155
|
+
description={d.skillMarketDesc}
|
|
156
|
+
comingSoonLabel={d.comingSoon}
|
|
157
|
+
/>
|
|
158
|
+
|
|
159
|
+
{/* View all link */}
|
|
160
|
+
<div className="px-4 py-3 mt-2">
|
|
161
|
+
<Link
|
|
162
|
+
href="/explore"
|
|
163
|
+
className="inline-flex items-center gap-1.5 text-xs text-[var(--amber)] hover:opacity-80 transition-opacity"
|
|
164
|
+
>
|
|
165
|
+
{d.viewAll}
|
|
166
|
+
<ExternalLink size={11} />
|
|
167
|
+
</Link>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -1,112 +1,172 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
-
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { ChevronDown, ChevronRight, Sun, Moon, Monitor, Type, Columns3, Globe } from 'lucide-react';
|
|
5
5
|
import { Locale } from '@/lib/i18n';
|
|
6
6
|
import { CONTENT_WIDTHS, FONTS, AppearanceTabProps } from './types';
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
/* ── Segmented Control ── */
|
|
9
|
+
function SegmentedControl<T extends string>({ options, value, onChange }: {
|
|
10
|
+
options: { value: T; label: string; icon?: React.ReactNode }[];
|
|
11
|
+
value: T;
|
|
12
|
+
onChange: (v: T) => void;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex rounded-lg border border-border bg-muted/30 p-0.5 gap-0.5">
|
|
16
|
+
{options.map(opt => (
|
|
17
|
+
<button
|
|
18
|
+
key={opt.value}
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={() => onChange(opt.value)}
|
|
21
|
+
className={`flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all ${
|
|
22
|
+
value === opt.value
|
|
23
|
+
? 'bg-card text-foreground shadow-sm'
|
|
24
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
25
|
+
}`}
|
|
26
|
+
>
|
|
27
|
+
{opt.icon}
|
|
28
|
+
{opt.label}
|
|
29
|
+
</button>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* ── Setting Group ── */
|
|
36
|
+
function SettingGroup({ icon, label, children }: { icon: React.ReactNode; label: string; children: React.ReactNode }) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-2.5">
|
|
39
|
+
<div className="flex items-center gap-2">
|
|
40
|
+
<span className="text-muted-foreground">{icon}</span>
|
|
41
|
+
<span className="text-xs font-medium text-foreground">{label}</span>
|
|
42
|
+
</div>
|
|
43
|
+
{children}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
8
47
|
|
|
9
48
|
export function AppearanceTab({ font, setFont, contentWidth, setContentWidth, dark, setDark, locale, setLocale, t }: AppearanceTabProps) {
|
|
10
49
|
const [showShortcuts, setShowShortcuts] = useState(false);
|
|
50
|
+
const [themePref, setThemePref] = useState<string>(() =>
|
|
51
|
+
typeof window !== 'undefined' ? (localStorage.getItem('theme') ?? 'system') : 'system'
|
|
52
|
+
);
|
|
53
|
+
const [localePref, setLocalePref] = useState<string>(() =>
|
|
54
|
+
typeof window !== 'undefined' ? (localStorage.getItem('locale') ?? 'system') : 'system'
|
|
55
|
+
);
|
|
56
|
+
const a = t.settings.appearance;
|
|
11
57
|
|
|
12
58
|
return (
|
|
13
|
-
<div className="space-y-
|
|
14
|
-
|
|
15
|
-
|
|
59
|
+
<div className="space-y-6">
|
|
60
|
+
{/* Font */}
|
|
61
|
+
<SettingGroup icon={<Type size={14} />} label={a.readingFont}>
|
|
62
|
+
<div className="flex flex-wrap gap-1.5">
|
|
16
63
|
{FONTS.map(f => (
|
|
17
|
-
<
|
|
64
|
+
<button
|
|
65
|
+
key={f.value}
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={() => setFont(f.value)}
|
|
68
|
+
className={`px-3 py-1.5 text-xs rounded-lg border transition-all ${
|
|
69
|
+
font === f.value
|
|
70
|
+
? 'border-amber-500 bg-amber-500/10 text-foreground font-medium shadow-sm'
|
|
71
|
+
: 'border-border text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
72
|
+
}`}
|
|
73
|
+
style={{ fontFamily: f.style.fontFamily }}
|
|
74
|
+
>
|
|
75
|
+
{f.label}
|
|
76
|
+
</button>
|
|
18
77
|
))}
|
|
19
|
-
</
|
|
20
|
-
<p
|
|
21
|
-
|
|
78
|
+
</div>
|
|
79
|
+
<p
|
|
80
|
+
className="text-sm text-muted-foreground leading-relaxed px-0.5 mt-1"
|
|
81
|
+
style={{ fontFamily: FONTS.find(f => f.value === font)?.style.fontFamily }}
|
|
82
|
+
>
|
|
83
|
+
{a.fontPreview}
|
|
22
84
|
</p>
|
|
23
|
-
</
|
|
85
|
+
</SettingGroup>
|
|
24
86
|
|
|
25
|
-
|
|
26
|
-
|
|
87
|
+
{/* Content Width */}
|
|
88
|
+
<SettingGroup icon={<Columns3 size={14} />} label={a.contentWidth}>
|
|
89
|
+
<div className="flex flex-wrap gap-1.5">
|
|
27
90
|
{CONTENT_WIDTHS.map(w => (
|
|
28
91
|
<button
|
|
29
92
|
key={w.value}
|
|
30
93
|
type="button"
|
|
31
94
|
onClick={() => setContentWidth(w.value)}
|
|
32
|
-
className={`px-3 py-
|
|
95
|
+
className={`px-3 py-1.5 text-xs rounded-lg border transition-all ${
|
|
33
96
|
contentWidth === w.value
|
|
34
|
-
? 'border-amber-500 bg-amber-500/10 text-foreground'
|
|
35
|
-
: 'border-border text-muted-foreground hover:
|
|
97
|
+
? 'border-amber-500 bg-amber-500/10 text-foreground font-medium shadow-sm'
|
|
98
|
+
: 'border-border text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
36
99
|
}`}
|
|
37
100
|
>
|
|
38
101
|
{w.label}
|
|
39
102
|
</button>
|
|
40
103
|
))}
|
|
41
104
|
</div>
|
|
42
|
-
</
|
|
105
|
+
</SettingGroup>
|
|
43
106
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{ value: '
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
>
|
|
65
|
-
{opt.label}
|
|
66
|
-
</button>
|
|
67
|
-
))}
|
|
68
|
-
</div>
|
|
69
|
-
</Field>
|
|
107
|
+
{/* Theme */}
|
|
108
|
+
<SettingGroup icon={<Sun size={14} />} label={a.colorTheme}>
|
|
109
|
+
<SegmentedControl
|
|
110
|
+
options={[
|
|
111
|
+
{ value: 'system', label: a.system, icon: <Monitor size={12} /> },
|
|
112
|
+
{ value: 'dark', label: a.dark, icon: <Moon size={12} /> },
|
|
113
|
+
{ value: 'light', label: a.light, icon: <Sun size={12} /> },
|
|
114
|
+
]}
|
|
115
|
+
value={themePref}
|
|
116
|
+
onChange={v => {
|
|
117
|
+
setThemePref(v);
|
|
118
|
+
localStorage.setItem('theme', v);
|
|
119
|
+
const isDark = v === 'system'
|
|
120
|
+
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
121
|
+
: v === 'dark';
|
|
122
|
+
setDark(isDark);
|
|
123
|
+
document.documentElement.classList.toggle('dark', isDark);
|
|
124
|
+
}}
|
|
125
|
+
/>
|
|
126
|
+
</SettingGroup>
|
|
70
127
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
{/* Language */}
|
|
129
|
+
<SettingGroup icon={<Globe size={14} />} label={a.language}>
|
|
130
|
+
<SegmentedControl
|
|
131
|
+
options={[
|
|
132
|
+
{ value: 'system', label: a.system, icon: <Monitor size={12} /> },
|
|
133
|
+
{ value: 'en', label: 'English' },
|
|
134
|
+
{ value: 'zh', label: '中文' },
|
|
135
|
+
]}
|
|
136
|
+
value={localePref}
|
|
137
|
+
onChange={v => {
|
|
138
|
+
setLocalePref(v);
|
|
139
|
+
localStorage.setItem('locale', v);
|
|
140
|
+
const resolved: Locale = v === 'system'
|
|
141
|
+
? (navigator.language.startsWith('zh') ? 'zh' : 'en')
|
|
142
|
+
: v as Locale;
|
|
143
|
+
setLocale(resolved);
|
|
144
|
+
// Sync cookie for SSR (write resolved value, not 'system')
|
|
145
|
+
document.cookie = `locale=${resolved};path=/;max-age=31536000;SameSite=Lax`;
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
148
|
+
</SettingGroup>
|
|
89
149
|
|
|
90
|
-
<p className="text-xs text-muted-foreground">{
|
|
150
|
+
<p className="text-xs text-muted-foreground/60 px-0.5">{a.browserNote}</p>
|
|
91
151
|
|
|
92
152
|
{/* Keyboard Shortcuts */}
|
|
93
153
|
<div className="border-t border-border pt-4">
|
|
94
154
|
<button
|
|
95
155
|
type="button"
|
|
96
156
|
onClick={() => setShowShortcuts(!showShortcuts)}
|
|
97
|
-
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground
|
|
157
|
+
className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground hover:text-foreground transition-colors"
|
|
98
158
|
>
|
|
99
159
|
{showShortcuts ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
100
160
|
{t.settings.tabs.shortcuts}
|
|
101
161
|
</button>
|
|
102
162
|
{showShortcuts && (
|
|
103
|
-
<div className="mt-3 space-y-
|
|
163
|
+
<div className="mt-3 space-y-0.5">
|
|
104
164
|
{t.shortcuts.map((s: { readonly description: string; readonly keys: readonly string[] }, i: number) => (
|
|
105
|
-
<div key={i} className="flex items-center justify-between py-
|
|
106
|
-
<span className="text-
|
|
165
|
+
<div key={i} className="flex items-center justify-between py-1.5 px-1">
|
|
166
|
+
<span className="text-xs text-foreground">{s.description}</span>
|
|
107
167
|
<div className="flex items-center gap-1">
|
|
108
168
|
{s.keys.map((k: string, j: number) => (
|
|
109
|
-
<kbd key={j} className="px-
|
|
169
|
+
<kbd key={j} className="px-1.5 py-0.5 text-2xs font-mono bg-muted border border-border rounded text-muted-foreground">{k}</kbd>
|
|
110
170
|
))}
|
|
111
171
|
</div>
|
|
112
172
|
</div>
|
|
@@ -32,6 +32,24 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
32
32
|
const [contentWidth, setContentWidth] = useState('780px');
|
|
33
33
|
const [dark, setDark] = useState(true);
|
|
34
34
|
|
|
35
|
+
// Update available badge on Update tab
|
|
36
|
+
const [hasUpdate, setHasUpdate] = useState(() => {
|
|
37
|
+
if (typeof window === 'undefined') return false;
|
|
38
|
+
const dismissed = localStorage.getItem('mindos_update_dismissed');
|
|
39
|
+
const latest = localStorage.getItem('mindos_update_latest');
|
|
40
|
+
return !!latest && latest !== dismissed;
|
|
41
|
+
});
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const onAvail = () => setHasUpdate(true);
|
|
44
|
+
const onDismiss = () => setHasUpdate(false);
|
|
45
|
+
window.addEventListener('mindos:update-available', onAvail);
|
|
46
|
+
window.addEventListener('mindos:update-dismissed', onDismiss);
|
|
47
|
+
return () => {
|
|
48
|
+
window.removeEventListener('mindos:update-available', onAvail);
|
|
49
|
+
window.removeEventListener('mindos:update-dismissed', onDismiss);
|
|
50
|
+
};
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
35
53
|
const isPanel = variant === 'panel';
|
|
36
54
|
|
|
37
55
|
// Init data when becoming visible
|
|
@@ -131,13 +149,13 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
131
149
|
const env = data?.envOverrides ?? {};
|
|
132
150
|
const iconSize = isPanel ? 12 : 13;
|
|
133
151
|
|
|
134
|
-
const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
|
152
|
+
const TABS: { id: Tab; label: string; icon: React.ReactNode; badge?: boolean }[] = [
|
|
135
153
|
{ id: 'ai', label: t.settings.tabs.ai, icon: <Sparkles size={iconSize} /> },
|
|
136
154
|
{ id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP & Skills', icon: <Plug size={iconSize} /> },
|
|
137
155
|
{ id: 'knowledge', label: t.settings.tabs.knowledge, icon: <Settings size={iconSize} /> },
|
|
138
156
|
{ id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
|
|
139
157
|
{ id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={iconSize} /> },
|
|
140
|
-
{ id: 'update', label: t.settings.tabs.update ?? 'Update', icon: <Download size={iconSize}
|
|
158
|
+
{ id: 'update', label: t.settings.tabs.update ?? 'Update', icon: <Download size={iconSize} />, badge: hasUpdate },
|
|
141
159
|
];
|
|
142
160
|
|
|
143
161
|
const activeTabLabel = TABS.find(t2 => t2.id === tab)?.label ?? '';
|
|
@@ -233,6 +251,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
233
251
|
>
|
|
234
252
|
{tabItem.icon}
|
|
235
253
|
{tabItem.label}
|
|
254
|
+
{tabItem.badge && <span className="w-1.5 h-1.5 rounded-full bg-error shrink-0" />}
|
|
236
255
|
</button>
|
|
237
256
|
))}
|
|
238
257
|
</div>
|
|
@@ -283,6 +302,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
283
302
|
>
|
|
284
303
|
{tabItem.icon}
|
|
285
304
|
{tabItem.label}
|
|
305
|
+
{tabItem.badge && <span className="w-1.5 h-1.5 rounded-full bg-error shrink-0" />}
|
|
286
306
|
</button>
|
|
287
307
|
))}
|
|
288
308
|
</div>
|
|
@@ -314,6 +334,7 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
314
334
|
)}
|
|
315
335
|
{tabItem.icon}
|
|
316
336
|
{tabItem.label}
|
|
337
|
+
{tabItem.badge && <span className="ml-auto w-1.5 h-1.5 rounded-full bg-error shrink-0" />}
|
|
317
338
|
</button>
|
|
318
339
|
))}
|
|
319
340
|
</nav>
|
|
@@ -15,12 +15,22 @@ const LocaleContext = createContext<LocaleContextValue>({
|
|
|
15
15
|
t: messages['en'],
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
/** Read locale from localStorage (canonical client source), resolving 'system' */
|
|
18
19
|
function getLocaleSnapshot(): Locale {
|
|
19
20
|
const saved = localStorage.getItem('locale');
|
|
20
|
-
|
|
21
|
+
if (saved === 'zh') return 'zh';
|
|
22
|
+
if (saved === 'en') return 'en';
|
|
23
|
+
// 'system' or null — detect from browser
|
|
24
|
+
return navigator.language.startsWith('zh') ? 'zh' : 'en';
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
interface LocaleProviderProps {
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
/** Locale read from cookie on the server — ensures SSR matches client hydration */
|
|
30
|
+
ssrLocale?: Locale;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function LocaleProvider({ children, ssrLocale = 'en' }: LocaleProviderProps) {
|
|
24
34
|
const locale = useSyncExternalStore(
|
|
25
35
|
(onStoreChange) => {
|
|
26
36
|
const listener = () => onStoreChange();
|
|
@@ -28,11 +38,13 @@ export function LocaleProvider({ children }: { children: ReactNode }) {
|
|
|
28
38
|
return () => window.removeEventListener('mindos-locale-change', listener);
|
|
29
39
|
},
|
|
30
40
|
getLocaleSnapshot,
|
|
31
|
-
() =>
|
|
41
|
+
() => ssrLocale,
|
|
32
42
|
);
|
|
33
43
|
|
|
34
44
|
const setLocale = (l: Locale) => {
|
|
35
|
-
|
|
45
|
+
// Only write resolved locale to cookie + html lang (not localStorage — caller manages that)
|
|
46
|
+
document.cookie = `locale=${l};path=/;max-age=31536000;SameSite=Lax`;
|
|
47
|
+
document.documentElement.lang = l === 'zh' ? 'zh' : 'en';
|
|
36
48
|
window.dispatchEvent(new Event('mindos-locale-change'));
|
|
37
49
|
};
|
|
38
50
|
|
package/app/lib/actions.ts
CHANGED
|
@@ -46,8 +46,9 @@ export async function renameFileAction(oldPath: string, newName: string): Promis
|
|
|
46
46
|
*/
|
|
47
47
|
export async function createSpaceAction(
|
|
48
48
|
name: string,
|
|
49
|
-
description: string
|
|
50
|
-
|
|
49
|
+
description: string,
|
|
50
|
+
parentPath: string = ''
|
|
51
|
+
): Promise<{ success: boolean; path?: string; error?: string }> {
|
|
51
52
|
try {
|
|
52
53
|
const trimmed = name.trim();
|
|
53
54
|
if (!trimmed) return { success: false, error: 'Space name is required' };
|
|
@@ -55,15 +56,25 @@ export async function createSpaceAction(
|
|
|
55
56
|
return { success: false, error: 'Space name must not contain path separators' };
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// Sanitize parentPath — reject traversal attempts
|
|
60
|
+
const cleanParent = parentPath.replace(/\/+$/, '').trim();
|
|
61
|
+
if (cleanParent.includes('..') || cleanParent.startsWith('/') || cleanParent.includes('\\')) {
|
|
62
|
+
return { success: false, error: 'Invalid parent path' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Build full path: parentPath + name
|
|
66
|
+
const prefix = cleanParent ? cleanParent + '/' : '';
|
|
67
|
+
const fullPath = `${prefix}${trimmed}`;
|
|
68
|
+
|
|
58
69
|
// Strip emoji for clean title in README content
|
|
59
70
|
const cleanName = trimmed.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}\s]+/u, '') || trimmed;
|
|
60
71
|
const desc = description.trim() || '(Describe the purpose and usage of this space.)';
|
|
61
|
-
const readmeContent = `# ${cleanName}\n\n${desc}\n\n## 📁 Structure\n\n\`\`\`bash\n${
|
|
72
|
+
const readmeContent = `# ${cleanName}\n\n${desc}\n\n## 📁 Structure\n\n\`\`\`bash\n${fullPath}/\n├── INSTRUCTION.md\n├── README.md\n└── (your files here)\n\`\`\`\n\n## 💡 Usage\n\n(Add usage guidelines for this space.)\n`;
|
|
62
73
|
|
|
63
74
|
// createFile triggers scaffoldIfNewSpace → auto-generates INSTRUCTION.md
|
|
64
|
-
createFile(`${
|
|
75
|
+
createFile(`${fullPath}/README.md`, readmeContent);
|
|
65
76
|
revalidatePath('/', 'layout');
|
|
66
|
-
return { success: true };
|
|
77
|
+
return { success: true, path: fullPath };
|
|
67
78
|
} catch (err) {
|
|
68
79
|
const msg = err instanceof Error ? err.message : 'Failed to create space';
|
|
69
80
|
// Make "already exists" error more user-friendly
|
package/app/lib/i18n-en.ts
CHANGED
|
@@ -16,14 +16,23 @@ export const en = {
|
|
|
16
16
|
other: 'Other',
|
|
17
17
|
newSpace: 'New Space',
|
|
18
18
|
spaceName: 'Space name',
|
|
19
|
-
spaceDescription: '
|
|
19
|
+
spaceDescription: 'Description',
|
|
20
|
+
spaceDescPlaceholder: 'Describe the purpose of this space',
|
|
21
|
+
spaceNameNoSlash: 'Name cannot contain / or \\',
|
|
22
|
+
spaceExists: 'A space with this name already exists',
|
|
23
|
+
spaceCreateFailed: 'Failed to create space',
|
|
24
|
+
noSpacesYet: 'No spaces yet. Create one to organize your knowledge.',
|
|
25
|
+
spaceLocation: 'Location',
|
|
26
|
+
rootLevel: 'Root',
|
|
27
|
+
optional: 'optional',
|
|
20
28
|
createSpace: 'Create',
|
|
21
29
|
cancelCreate: 'Cancel',
|
|
22
30
|
continueEditing: 'Continue editing',
|
|
23
31
|
newNote: 'New Notes',
|
|
24
|
-
plugins: '
|
|
32
|
+
plugins: 'Extensions',
|
|
25
33
|
showMore: 'Show more',
|
|
26
34
|
showLess: 'Show less',
|
|
35
|
+
viewAll: 'View all',
|
|
27
36
|
createToActivate: 'Create {file} to activate',
|
|
28
37
|
shortcuts: {
|
|
29
38
|
searchFiles: 'Search files',
|
|
@@ -47,6 +56,7 @@ export const en = {
|
|
|
47
56
|
settingsTitle: 'Settings',
|
|
48
57
|
plugins: 'Plugins',
|
|
49
58
|
agents: 'Agents',
|
|
59
|
+
discover: 'Discover',
|
|
50
60
|
syncLabel: 'Sync',
|
|
51
61
|
collapseTitle: 'Collapse sidebar',
|
|
52
62
|
expandTitle: 'Expand sidebar',
|
|
@@ -143,6 +153,18 @@ export const en = {
|
|
|
143
153
|
noFile: 'Entry file not found',
|
|
144
154
|
createFile: 'Create {file} to activate',
|
|
145
155
|
},
|
|
156
|
+
discover: {
|
|
157
|
+
title: 'Discover',
|
|
158
|
+
useCases: 'Use Cases',
|
|
159
|
+
useCasesDesc: 'Try MindOS scenarios hands-on',
|
|
160
|
+
pluginMarket: 'Plugin Market',
|
|
161
|
+
pluginMarketDesc: 'Extend how files are rendered and edited',
|
|
162
|
+
skillMarket: 'Skill Market',
|
|
163
|
+
skillMarketDesc: 'Add new abilities to your AI agents',
|
|
164
|
+
comingSoon: 'Coming soon',
|
|
165
|
+
viewAll: 'View all use cases',
|
|
166
|
+
tryIt: 'Try',
|
|
167
|
+
},
|
|
146
168
|
},
|
|
147
169
|
shortcutPanel: {
|
|
148
170
|
title: 'Keyboard Shortcuts',
|
|
@@ -175,6 +197,10 @@ export const en = {
|
|
|
175
197
|
fileCount: (n: number) => `${n} files`,
|
|
176
198
|
newFile: 'New file',
|
|
177
199
|
},
|
|
200
|
+
view: {
|
|
201
|
+
saveDirectory: 'Directory',
|
|
202
|
+
saveFileName: 'File name',
|
|
203
|
+
},
|
|
178
204
|
findInPage: {
|
|
179
205
|
placeholder: 'Find in document…',
|
|
180
206
|
matchCount: (current: number, total: number) => `${current} of ${total}`,
|
|
@@ -222,6 +248,7 @@ export const en = {
|
|
|
222
248
|
readingFont: 'Reading font',
|
|
223
249
|
contentWidth: 'Content width',
|
|
224
250
|
colorTheme: 'Color theme',
|
|
251
|
+
system: 'System',
|
|
225
252
|
dark: 'Dark',
|
|
226
253
|
light: 'Light',
|
|
227
254
|
language: 'Language',
|