@geminilight/mindos 0.5.52 → 0.5.55
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 +7 -7
- package/README_zh.md +5 -5
- package/app/app/echo/[segment]/page.tsx +15 -0
- package/app/app/echo/page.tsx +6 -0
- package/app/components/ActivityBar.tsx +3 -2
- package/app/components/Panel.tsx +1 -0
- package/app/components/RightAgentDetailPanel.tsx +121 -0
- package/app/components/RightAskPanel.tsx +14 -11
- package/app/components/SidebarLayout.tsx +69 -5
- package/app/components/ask/AskContent.tsx +10 -2
- package/app/components/echo/EchoHero.tsx +55 -0
- package/app/components/echo/EchoInsightCollapsible.tsx +184 -0
- package/app/components/echo/EchoPageSections.tsx +86 -0
- package/app/components/echo/EchoSegmentNav.tsx +58 -0
- package/app/components/echo/EchoSegmentPageClient.tsx +265 -0
- package/app/components/panels/AgentsPanel.tsx +156 -178
- package/app/components/panels/AgentsPanelAgentDetail.tsx +193 -0
- package/app/components/panels/AgentsPanelAgentGroups.tsx +116 -0
- package/app/components/panels/AgentsPanelAgentListRow.tsx +101 -0
- package/app/components/panels/AgentsPanelHubNav.tsx +48 -0
- package/app/components/panels/DiscoverPanel.tsx +6 -46
- package/app/components/panels/EchoPanel.tsx +49 -0
- package/app/components/panels/PanelNavRow.tsx +68 -0
- package/app/components/panels/agents-panel-resolve-status.ts +13 -0
- package/app/hooks/useSettingsAiAvailable.ts +29 -0
- package/app/lib/echo-insight-prompt.ts +44 -0
- package/app/lib/echo-segments.ts +27 -0
- package/app/lib/i18n-en.ts +62 -2
- package/app/lib/i18n-zh.ts +59 -2
- package/app/lib/settings-ai-client.ts +26 -0
- package/app/next-env.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
|
+
import type { AgentInfo } from '../settings/types';
|
|
5
|
+
import type { McpContextValue } from '@/hooks/useMcpData';
|
|
6
|
+
import AgentsPanelAgentListRow, { type AgentsPanelAgentListRowCopy } from './AgentsPanelAgentListRow';
|
|
7
|
+
|
|
8
|
+
type AgentsCopy = {
|
|
9
|
+
rosterLabel: string;
|
|
10
|
+
sectionConnected: string;
|
|
11
|
+
sectionDetected: string;
|
|
12
|
+
sectionNotDetected: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function AgentsPanelAgentGroups({
|
|
16
|
+
connected,
|
|
17
|
+
detected,
|
|
18
|
+
notFound,
|
|
19
|
+
onOpenDetail,
|
|
20
|
+
selectedAgentKey,
|
|
21
|
+
mcp,
|
|
22
|
+
listCopy,
|
|
23
|
+
showNotDetected,
|
|
24
|
+
setShowNotDetected,
|
|
25
|
+
p,
|
|
26
|
+
}: {
|
|
27
|
+
connected: AgentInfo[];
|
|
28
|
+
detected: AgentInfo[];
|
|
29
|
+
notFound: AgentInfo[];
|
|
30
|
+
onOpenDetail?: (key: string) => void;
|
|
31
|
+
selectedAgentKey?: string | null;
|
|
32
|
+
mcp: Pick<McpContextValue, 'installAgent'>;
|
|
33
|
+
listCopy: AgentsPanelAgentListRowCopy;
|
|
34
|
+
showNotDetected: boolean;
|
|
35
|
+
setShowNotDetected: (v: boolean | ((prev: boolean) => boolean)) => void;
|
|
36
|
+
p: AgentsCopy;
|
|
37
|
+
}) {
|
|
38
|
+
const open = onOpenDetail ?? (() => {});
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
<div className="px-0 py-1 mb-0.5">
|
|
43
|
+
<span className="text-2xs font-semibold text-muted-foreground uppercase tracking-wider">{p.rosterLabel}</span>
|
|
44
|
+
</div>
|
|
45
|
+
{connected.length > 0 && (
|
|
46
|
+
<section className="mb-3">
|
|
47
|
+
<h3 className="text-[11px] font-medium text-muted-foreground/90 uppercase tracking-wider mb-2 pl-0.5">
|
|
48
|
+
{p.sectionConnected} ({connected.length})
|
|
49
|
+
</h3>
|
|
50
|
+
<div className="space-y-1.5">
|
|
51
|
+
{connected.map(agent => (
|
|
52
|
+
<AgentsPanelAgentListRow
|
|
53
|
+
key={agent.key}
|
|
54
|
+
agent={agent}
|
|
55
|
+
agentStatus="connected"
|
|
56
|
+
selected={selectedAgentKey === agent.key}
|
|
57
|
+
onOpenDetail={() => open(agent.key)}
|
|
58
|
+
onInstallAgent={mcp.installAgent}
|
|
59
|
+
copy={listCopy}
|
|
60
|
+
/>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
</section>
|
|
64
|
+
)}
|
|
65
|
+
|
|
66
|
+
{detected.length > 0 && (
|
|
67
|
+
<section className="mb-3">
|
|
68
|
+
<h3 className="text-[11px] font-medium text-muted-foreground/90 uppercase tracking-wider mb-2 pl-0.5">
|
|
69
|
+
{p.sectionDetected} ({detected.length})
|
|
70
|
+
</h3>
|
|
71
|
+
<div className="space-y-1.5">
|
|
72
|
+
{detected.map(agent => (
|
|
73
|
+
<AgentsPanelAgentListRow
|
|
74
|
+
key={agent.key}
|
|
75
|
+
agent={agent}
|
|
76
|
+
agentStatus="detected"
|
|
77
|
+
selected={selectedAgentKey === agent.key}
|
|
78
|
+
onOpenDetail={() => open(agent.key)}
|
|
79
|
+
onInstallAgent={mcp.installAgent}
|
|
80
|
+
copy={listCopy}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
</section>
|
|
85
|
+
)}
|
|
86
|
+
|
|
87
|
+
{notFound.length > 0 && (
|
|
88
|
+
<section>
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
onClick={() => setShowNotDetected(!showNotDetected)}
|
|
92
|
+
className="flex items-center gap-1 text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm pl-0.5"
|
|
93
|
+
>
|
|
94
|
+
{showNotDetected ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
|
95
|
+
{p.sectionNotDetected} ({notFound.length})
|
|
96
|
+
</button>
|
|
97
|
+
{showNotDetected && (
|
|
98
|
+
<div className="space-y-1.5">
|
|
99
|
+
{notFound.map(agent => (
|
|
100
|
+
<AgentsPanelAgentListRow
|
|
101
|
+
key={agent.key}
|
|
102
|
+
agent={agent}
|
|
103
|
+
agentStatus="notFound"
|
|
104
|
+
selected={selectedAgentKey === agent.key}
|
|
105
|
+
onOpenDetail={() => open(agent.key)}
|
|
106
|
+
onInstallAgent={mcp.installAgent}
|
|
107
|
+
copy={listCopy}
|
|
108
|
+
/>
|
|
109
|
+
))}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</section>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { ChevronRight, Loader2 } from 'lucide-react';
|
|
5
|
+
import type { AgentInfo } from '../settings/types';
|
|
6
|
+
|
|
7
|
+
export type AgentsPanelAgentListStatus = 'connected' | 'detected' | 'notFound';
|
|
8
|
+
|
|
9
|
+
export interface AgentsPanelAgentListRowCopy {
|
|
10
|
+
installing: string;
|
|
11
|
+
install: (name: string) => string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default function AgentsPanelAgentListRow({
|
|
15
|
+
agent,
|
|
16
|
+
agentStatus,
|
|
17
|
+
selected = false,
|
|
18
|
+
onOpenDetail,
|
|
19
|
+
onInstallAgent,
|
|
20
|
+
copy,
|
|
21
|
+
}: {
|
|
22
|
+
agent: AgentInfo;
|
|
23
|
+
agentStatus: AgentsPanelAgentListStatus;
|
|
24
|
+
selected?: boolean;
|
|
25
|
+
onOpenDetail: () => void;
|
|
26
|
+
onInstallAgent: (key: string) => Promise<boolean>;
|
|
27
|
+
copy: AgentsPanelAgentListRowCopy;
|
|
28
|
+
}) {
|
|
29
|
+
const dot =
|
|
30
|
+
agentStatus === 'connected' ? 'bg-emerald-500' : agentStatus === 'detected' ? 'bg-amber-500' : 'bg-zinc-400';
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
className={`
|
|
35
|
+
group flex items-center gap-0 rounded-xl border transition-all duration-150
|
|
36
|
+
${selected
|
|
37
|
+
? 'border-border ring-2 ring-ring/50 bg-[var(--amber-dim)]/45'
|
|
38
|
+
: 'border-border/70 bg-card/50 hover:border-border hover:bg-muted/25'}
|
|
39
|
+
`}
|
|
40
|
+
>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={onOpenDetail}
|
|
44
|
+
className="flex flex-1 min-w-0 items-center gap-2.5 text-left rounded-xl pl-3 pr-2 py-2.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
45
|
+
>
|
|
46
|
+
<span className={`w-2 h-2 rounded-full shrink-0 ring-2 ring-background ${dot}`} />
|
|
47
|
+
<span className="text-sm font-medium text-foreground truncate leading-tight">{agent.name}</span>
|
|
48
|
+
{agentStatus === 'connected' && agent.transport && (
|
|
49
|
+
<span className="text-2xs font-mono tabular-nums px-1.5 py-0.5 rounded-md bg-muted/90 text-muted-foreground shrink-0 border border-border/50">
|
|
50
|
+
{agent.transport}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
<span className="flex-1 min-w-[4px]" />
|
|
54
|
+
<ChevronRight
|
|
55
|
+
size={15}
|
|
56
|
+
className={`shrink-0 transition-opacity duration-150 ${selected ? 'text-[var(--amber)] opacity-90' : 'text-muted-foreground/45 group-hover:text-muted-foreground/80'}`}
|
|
57
|
+
aria-hidden
|
|
58
|
+
/>
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
{agentStatus === 'detected' && (
|
|
62
|
+
<div className="pr-2 py-2 shrink-0">
|
|
63
|
+
<AgentInstallButton agentKey={agent.key} agentName={agent.name} onInstallAgent={onInstallAgent} copy={copy} />
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function AgentInstallButton({
|
|
71
|
+
agentKey,
|
|
72
|
+
agentName,
|
|
73
|
+
onInstallAgent,
|
|
74
|
+
copy,
|
|
75
|
+
}: {
|
|
76
|
+
agentKey: string;
|
|
77
|
+
agentName: string;
|
|
78
|
+
onInstallAgent: (key: string) => Promise<boolean>;
|
|
79
|
+
copy: AgentsPanelAgentListRowCopy;
|
|
80
|
+
}) {
|
|
81
|
+
const [installing, setInstalling] = useState(false);
|
|
82
|
+
|
|
83
|
+
const handleInstall = async (e: React.MouseEvent) => {
|
|
84
|
+
e.stopPropagation();
|
|
85
|
+
setInstalling(true);
|
|
86
|
+
await onInstallAgent(agentKey);
|
|
87
|
+
setInstalling(false);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
onClick={handleInstall}
|
|
94
|
+
disabled={installing}
|
|
95
|
+
className="flex items-center gap-1 px-2 py-1.5 text-2xs rounded-lg font-medium text-[var(--amber-foreground)] disabled:opacity-50 transition-colors shrink-0 bg-[var(--amber)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
96
|
+
>
|
|
97
|
+
{installing ? <Loader2 size={10} className="animate-spin" /> : null}
|
|
98
|
+
{installing ? copy.installing : copy.install(agentName)}
|
|
99
|
+
</button>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type RefObject } from 'react';
|
|
4
|
+
import { LayoutDashboard, Server, Zap } from 'lucide-react';
|
|
5
|
+
import { PanelNavRow } from './PanelNavRow';
|
|
6
|
+
|
|
7
|
+
type HubCopy = {
|
|
8
|
+
navOverview: string;
|
|
9
|
+
navMcp: string;
|
|
10
|
+
navSkills: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function AgentsPanelHubNav({
|
|
14
|
+
copy,
|
|
15
|
+
connectedCount,
|
|
16
|
+
overviewRef,
|
|
17
|
+
skillsRef,
|
|
18
|
+
scrollTo,
|
|
19
|
+
openAdvancedConfig,
|
|
20
|
+
}: {
|
|
21
|
+
copy: HubCopy;
|
|
22
|
+
connectedCount: number;
|
|
23
|
+
overviewRef: RefObject<HTMLDivElement | null>;
|
|
24
|
+
skillsRef: RefObject<HTMLDivElement | null>;
|
|
25
|
+
scrollTo: (el: HTMLElement | null) => void;
|
|
26
|
+
openAdvancedConfig: () => void;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="py-2">
|
|
30
|
+
<PanelNavRow
|
|
31
|
+
icon={<LayoutDashboard size={14} className="text-[var(--amber)]" />}
|
|
32
|
+
title={copy.navOverview}
|
|
33
|
+
badge={<span className="text-2xs tabular-nums text-muted-foreground">{connectedCount}</span>}
|
|
34
|
+
onClick={() => scrollTo(overviewRef.current)}
|
|
35
|
+
/>
|
|
36
|
+
<PanelNavRow
|
|
37
|
+
icon={<Server size={14} className="text-muted-foreground" />}
|
|
38
|
+
title={copy.navMcp}
|
|
39
|
+
onClick={openAdvancedConfig}
|
|
40
|
+
/>
|
|
41
|
+
<PanelNavRow
|
|
42
|
+
icon={<Zap size={14} className="text-muted-foreground" />}
|
|
43
|
+
title={copy.navSkills}
|
|
44
|
+
onClick={() => scrollTo(skillsRef.current)}
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { Lightbulb, Blocks, Zap, LayoutTemplate, ChevronRight, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck } from 'lucide-react';
|
|
3
|
+
import { Lightbulb, Blocks, Zap, LayoutTemplate, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck } from 'lucide-react';
|
|
5
4
|
import PanelHeader from './PanelHeader';
|
|
5
|
+
import { PanelNavRow, ComingSoonBadge } from './PanelNavRow';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
import { useCases } from '@/components/explore/use-cases';
|
|
8
8
|
import { openAskModal } from '@/hooks/useAskModal';
|
|
@@ -13,46 +13,6 @@ interface DiscoverPanelProps {
|
|
|
13
13
|
onMaximize?: () => void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/** Navigation entry — clickable row linking to a page or showing coming soon */
|
|
17
|
-
function NavEntry({
|
|
18
|
-
icon,
|
|
19
|
-
title,
|
|
20
|
-
badge,
|
|
21
|
-
href,
|
|
22
|
-
onClick,
|
|
23
|
-
}: {
|
|
24
|
-
icon: React.ReactNode;
|
|
25
|
-
title: string;
|
|
26
|
-
badge?: React.ReactNode;
|
|
27
|
-
href?: string;
|
|
28
|
-
onClick?: () => void;
|
|
29
|
-
}) {
|
|
30
|
-
const content = (
|
|
31
|
-
<>
|
|
32
|
-
<span className="flex items-center justify-center w-7 h-7 rounded-md bg-muted shrink-0">{icon}</span>
|
|
33
|
-
<span className="text-sm font-medium text-foreground flex-1">{title}</span>
|
|
34
|
-
{badge}
|
|
35
|
-
<ChevronRight size={14} className="text-muted-foreground shrink-0" />
|
|
36
|
-
</>
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const className = "flex items-center gap-3 px-4 py-2.5 hover:bg-muted/50 transition-colors cursor-pointer";
|
|
40
|
-
|
|
41
|
-
if (href) {
|
|
42
|
-
return <Link href={href} className={className}>{content}</Link>;
|
|
43
|
-
}
|
|
44
|
-
return <button onClick={onClick} className={`${className} w-full`}>{content}</button>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Coming soon badge */
|
|
48
|
-
function ComingSoonBadge({ label }: { label: string }) {
|
|
49
|
-
return (
|
|
50
|
-
<span className="text-2xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground shrink-0">
|
|
51
|
-
{label}
|
|
52
|
-
</span>
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
16
|
/** Compact use case row */
|
|
57
17
|
function UseCaseRow({
|
|
58
18
|
icon,
|
|
@@ -112,23 +72,23 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
|
|
|
112
72
|
<div className="flex-1 overflow-y-auto min-h-0">
|
|
113
73
|
{/* Navigation entries */}
|
|
114
74
|
<div className="py-2">
|
|
115
|
-
<
|
|
75
|
+
<PanelNavRow
|
|
116
76
|
icon={<Lightbulb size={14} className="text-[var(--amber)]" />}
|
|
117
77
|
title={d.useCases}
|
|
118
78
|
badge={<span className="text-2xs tabular-nums text-muted-foreground">{useCases.length}</span>}
|
|
119
79
|
href="/explore"
|
|
120
80
|
/>
|
|
121
|
-
<
|
|
81
|
+
<PanelNavRow
|
|
122
82
|
icon={<Blocks size={14} className="text-muted-foreground" />}
|
|
123
83
|
title={d.pluginMarket}
|
|
124
84
|
badge={<ComingSoonBadge label={d.comingSoon} />}
|
|
125
85
|
/>
|
|
126
|
-
<
|
|
86
|
+
<PanelNavRow
|
|
127
87
|
icon={<Zap size={14} className="text-muted-foreground" />}
|
|
128
88
|
title={d.skillMarket}
|
|
129
89
|
badge={<ComingSoonBadge label={d.comingSoon} />}
|
|
130
90
|
/>
|
|
131
|
-
<
|
|
91
|
+
<PanelNavRow
|
|
132
92
|
icon={<LayoutTemplate size={14} className="text-muted-foreground" />}
|
|
133
93
|
title={d.spaceTemplates}
|
|
134
94
|
badge={<ComingSoonBadge label={d.comingSoon} />}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { UserRound, Bookmark, Sun, History, Brain } from 'lucide-react';
|
|
6
|
+
import PanelHeader from './PanelHeader';
|
|
7
|
+
import { PanelNavRow } from './PanelNavRow';
|
|
8
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
9
|
+
import { ECHO_SEGMENT_HREF, ECHO_SEGMENT_ORDER, type EchoSegment } from '@/lib/echo-segments';
|
|
10
|
+
|
|
11
|
+
interface EchoPanelProps {
|
|
12
|
+
active: boolean;
|
|
13
|
+
maximized?: boolean;
|
|
14
|
+
onMaximize?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function EchoPanel({ active, maximized, onMaximize }: EchoPanelProps) {
|
|
18
|
+
const { t } = useLocale();
|
|
19
|
+
const e = t.panels.echo;
|
|
20
|
+
const pathname = usePathname() ?? '';
|
|
21
|
+
|
|
22
|
+
const rowBySegment: Record<EchoSegment, { icon: ReactNode; title: string }> = {
|
|
23
|
+
'about-you': { icon: <UserRound size={14} />, title: e.aboutYouTitle },
|
|
24
|
+
continued: { icon: <Bookmark size={14} />, title: e.continuedTitle },
|
|
25
|
+
daily: { icon: <Sun size={14} />, title: e.dailyEchoTitle },
|
|
26
|
+
'past-you': { icon: <History size={14} />, title: e.pastYouTitle },
|
|
27
|
+
growth: { icon: <Brain size={14} />, title: e.intentGrowthTitle },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
32
|
+
<PanelHeader title={e.title} maximized={maximized} onMaximize={onMaximize} />
|
|
33
|
+
<div className="flex-1 overflow-y-auto min-h-0">
|
|
34
|
+
<div className="flex flex-col py-1">
|
|
35
|
+
{ECHO_SEGMENT_ORDER.map((segment) => {
|
|
36
|
+
const row = rowBySegment[segment];
|
|
37
|
+
const href = ECHO_SEGMENT_HREF[segment];
|
|
38
|
+
const isActive = pathname === href || pathname.startsWith(`${href}/`);
|
|
39
|
+
return (
|
|
40
|
+
<div key={segment} className="border-b border-border/60 last:border-b-0">
|
|
41
|
+
<PanelNavRow href={href} icon={row.icon} title={row.title} active={isActive} />
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
})}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { ChevronRight } from 'lucide-react';
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
/** Row matching Discover panel nav: icon tile, title, optional badge, chevron. */
|
|
9
|
+
export function PanelNavRow({
|
|
10
|
+
icon,
|
|
11
|
+
title,
|
|
12
|
+
badge,
|
|
13
|
+
href,
|
|
14
|
+
onClick,
|
|
15
|
+
active,
|
|
16
|
+
}: {
|
|
17
|
+
icon: ReactNode;
|
|
18
|
+
title: string;
|
|
19
|
+
badge?: React.ReactNode;
|
|
20
|
+
href?: string;
|
|
21
|
+
onClick?: () => void;
|
|
22
|
+
/** When true, row shows selected state (e.g. current Echo segment). */
|
|
23
|
+
active?: boolean;
|
|
24
|
+
}) {
|
|
25
|
+
const content = (
|
|
26
|
+
<>
|
|
27
|
+
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted">{icon}</span>
|
|
28
|
+
<span className="flex-1 text-left text-sm font-medium text-foreground">{title}</span>
|
|
29
|
+
{badge}
|
|
30
|
+
<ChevronRight size={14} className="shrink-0 text-muted-foreground" />
|
|
31
|
+
</>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const showRail = Boolean(active && href);
|
|
35
|
+
|
|
36
|
+
const className = cn(
|
|
37
|
+
'relative flex items-center gap-3 py-2.5 transition-colors duration-150 rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
38
|
+
showRail ? 'bg-[var(--amber-dim)]/40 pl-3.5 pr-4 text-foreground' : 'px-4',
|
|
39
|
+
href && !showRail && 'cursor-pointer hover:bg-muted/50',
|
|
40
|
+
showRail && 'cursor-default',
|
|
41
|
+
!href && 'cursor-pointer hover:bg-muted/50',
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (href) {
|
|
45
|
+
return (
|
|
46
|
+
<Link href={href} className={className} aria-current={active ? 'page' : undefined}>
|
|
47
|
+
{showRail ? (
|
|
48
|
+
<span
|
|
49
|
+
className="pointer-events-none absolute bottom-[22%] left-0 top-[22%] w-0.5 rounded-r-full bg-[var(--amber)]"
|
|
50
|
+
aria-hidden
|
|
51
|
+
/>
|
|
52
|
+
) : null}
|
|
53
|
+
{content}
|
|
54
|
+
</Link>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return (
|
|
58
|
+
<button type="button" onClick={onClick} className={cn(className, 'w-full')}>
|
|
59
|
+
{content}
|
|
60
|
+
</button>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ComingSoonBadge({ label }: { label: string }) {
|
|
65
|
+
return (
|
|
66
|
+
<span className="text-2xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground shrink-0">{label}</span>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type AgentsPanelAgentDetailStatus = 'connected' | 'detected' | 'notFound';
|
|
2
|
+
|
|
3
|
+
export function resolveAgentDetailStatus(
|
|
4
|
+
key: string,
|
|
5
|
+
connected: { key: string }[],
|
|
6
|
+
detected: { key: string }[],
|
|
7
|
+
notFound: { key: string }[],
|
|
8
|
+
): AgentsPanelAgentDetailStatus | null {
|
|
9
|
+
if (connected.some(a => a.key === key)) return 'connected';
|
|
10
|
+
if (detected.some(a => a.key === key)) return 'detected';
|
|
11
|
+
if (notFound.some(a => a.key === key)) return 'notFound';
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { isAiConfiguredForAsk, type SettingsJsonForAi } from '@/lib/settings-ai-client';
|
|
5
|
+
|
|
6
|
+
export function useSettingsAiAvailable(): { ready: boolean; loading: boolean } {
|
|
7
|
+
const [ready, setReady] = useState(false);
|
|
8
|
+
const [loading, setLoading] = useState(true);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let cancelled = false;
|
|
12
|
+
fetch('/api/settings', { cache: 'no-store' })
|
|
13
|
+
.then((r) => r.json())
|
|
14
|
+
.then((d: SettingsJsonForAi) => {
|
|
15
|
+
if (!cancelled) setReady(isAiConfiguredForAsk(d));
|
|
16
|
+
})
|
|
17
|
+
.catch(() => {
|
|
18
|
+
if (!cancelled) setReady(false);
|
|
19
|
+
})
|
|
20
|
+
.finally(() => {
|
|
21
|
+
if (!cancelled) setLoading(false);
|
|
22
|
+
});
|
|
23
|
+
return () => {
|
|
24
|
+
cancelled = true;
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
return { ready, loading };
|
|
29
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { EchoSegment } from '@/lib/echo-segments';
|
|
2
|
+
|
|
3
|
+
export function buildEchoInsightUserPrompt(opts: {
|
|
4
|
+
locale: 'en' | 'zh';
|
|
5
|
+
segment: EchoSegment;
|
|
6
|
+
segmentTitle: string;
|
|
7
|
+
factsHeading: string;
|
|
8
|
+
emptyTitle: string;
|
|
9
|
+
emptyBody: string;
|
|
10
|
+
continuedDrafts: string;
|
|
11
|
+
continuedTodos: string;
|
|
12
|
+
subEmptyHint: string;
|
|
13
|
+
dailyLineLabel: string;
|
|
14
|
+
dailyLine: string;
|
|
15
|
+
growthIntentLabel: string;
|
|
16
|
+
growthIntent: string;
|
|
17
|
+
}): string {
|
|
18
|
+
const lang = opts.locale === 'zh' ? 'Chinese' : 'English';
|
|
19
|
+
const lines: string[] = [
|
|
20
|
+
`Echo section: ${opts.segmentTitle}`,
|
|
21
|
+
`${opts.factsHeading}: ${opts.emptyTitle}`,
|
|
22
|
+
opts.emptyBody,
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
if (opts.segment === 'continued') {
|
|
26
|
+
lines.push(`${opts.continuedDrafts} — ${opts.subEmptyHint}`, `${opts.continuedTodos} — ${opts.subEmptyHint}`);
|
|
27
|
+
}
|
|
28
|
+
if (opts.segment === 'daily' && opts.dailyLine.trim()) {
|
|
29
|
+
lines.push(`${opts.dailyLineLabel}: ${opts.dailyLine.trim()}`);
|
|
30
|
+
}
|
|
31
|
+
if (opts.segment === 'growth' && opts.growthIntent.trim()) {
|
|
32
|
+
lines.push(`${opts.growthIntentLabel}: ${opts.growthIntent.trim()}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const context = lines.join('\n\n');
|
|
36
|
+
|
|
37
|
+
return `You are a reflective assistant inside MindOS Echo (a personal, local-first notes companion). The user is viewing one Echo section. Below is exactly what they see on screen right now—it may be an empty-state placeholder until indexing fills the list.
|
|
38
|
+
|
|
39
|
+
--- Visible context ---
|
|
40
|
+
${context}
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
Write a short insight in ${lang} as Markdown (under 220 words). Tone: warm, restrained, "quiet notebook"—not a hype coach. If the context is only generic empty copy, acknowledge that briefly and offer 2–3 reflection prompts instead of inventing files or facts. Do not claim you read their whole library. Prefer answering from this context; avoid unnecessary tool use.`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Echo content routes: segment slugs and validation.
|
|
3
|
+
* Keep in sync with `wiki/specs/spec-echo-content-pages.md`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const ECHO_SEGMENT_IDS = ['about-you', 'continued', 'daily', 'past-you', 'growth'] as const;
|
|
7
|
+
|
|
8
|
+
export type EchoSegment = (typeof ECHO_SEGMENT_IDS)[number];
|
|
9
|
+
|
|
10
|
+
export const ECHO_SEGMENT_ORDER: readonly EchoSegment[] = ECHO_SEGMENT_IDS;
|
|
11
|
+
|
|
12
|
+
/** App Router paths for each segment (single source for panel + in-page nav). */
|
|
13
|
+
export const ECHO_SEGMENT_HREF: Record<EchoSegment, string> = {
|
|
14
|
+
'about-you': '/echo/about-you',
|
|
15
|
+
continued: '/echo/continued',
|
|
16
|
+
daily: '/echo/daily',
|
|
17
|
+
'past-you': '/echo/past-you',
|
|
18
|
+
growth: '/echo/growth',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function isEchoSegment(value: string): value is EchoSegment {
|
|
22
|
+
return (ECHO_SEGMENT_IDS as readonly string[]).includes(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function defaultEchoSegment(): EchoSegment {
|
|
26
|
+
return 'about-you';
|
|
27
|
+
}
|