@geminilight/mindos 0.5.63 → 0.5.65
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 +4 -0
- package/README_zh.md +4 -0
- package/app/app/api/ask/route.ts +12 -0
- package/app/app/api/changes/route.ts +7 -1
- package/app/app/api/file/route.ts +9 -0
- package/app/app/api/mcp/agents/route.ts +27 -1
- package/app/app/api/mcp/install-skill/route.ts +9 -24
- package/app/app/api/skills/route.ts +18 -2
- package/app/app/api/tree-version/route.ts +8 -0
- package/app/app/layout.tsx +1 -0
- package/app/app/page.tsx +1 -2
- package/app/app/view/[...path]/ViewPageClient.tsx +0 -1
- package/app/components/ActivityBar.tsx +2 -2
- package/app/components/Backlinks.tsx +5 -5
- package/app/components/CreateSpaceModal.tsx +3 -2
- package/app/components/DirPicker.tsx +1 -1
- package/app/components/DirView.tsx +2 -3
- package/app/components/EditorWrapper.tsx +3 -3
- package/app/components/FileTree.tsx +25 -10
- package/app/components/GuideCard.tsx +4 -4
- package/app/components/HomeContent.tsx +44 -14
- package/app/components/MarkdownView.tsx +2 -2
- package/app/components/OnboardingView.tsx +1 -1
- package/app/components/Panel.tsx +1 -1
- package/app/components/RightAgentDetailPanel.tsx +2 -1
- package/app/components/RightAskPanel.tsx +1 -1
- package/app/components/SearchModal.tsx +10 -2
- package/app/components/SidebarLayout.tsx +36 -10
- package/app/components/ThemeToggle.tsx +1 -1
- package/app/components/agents/AgentDetailContent.tsx +454 -59
- package/app/components/agents/AgentsContentPage.tsx +89 -20
- package/app/components/agents/AgentsMcpSection.tsx +513 -85
- package/app/components/agents/AgentsOverviewSection.tsx +418 -59
- package/app/components/agents/AgentsPrimitives.tsx +335 -0
- package/app/components/agents/AgentsSkillsSection.tsx +746 -105
- package/app/components/agents/SkillDetailPopover.tsx +416 -0
- package/app/components/agents/agents-content-model.ts +308 -10
- package/app/components/ask/AskContent.tsx +34 -5
- package/app/components/ask/FileChip.tsx +1 -0
- package/app/components/ask/MentionPopover.tsx +13 -1
- package/app/components/ask/MessageList.tsx +5 -7
- package/app/components/ask/ToolCallBlock.tsx +4 -4
- package/app/components/changes/ChangesBanner.tsx +89 -13
- package/app/components/changes/ChangesContentPage.tsx +134 -51
- package/app/components/echo/EchoHero.tsx +10 -24
- package/app/components/echo/EchoInsightCollapsible.tsx +52 -43
- package/app/components/echo/EchoPageSections.tsx +13 -9
- package/app/components/echo/EchoSegmentNav.tsx +14 -11
- package/app/components/echo/EchoSegmentPageClient.tsx +64 -43
- package/app/components/explore/ExploreContent.tsx +3 -7
- package/app/components/explore/UseCaseCard.tsx +4 -15
- package/app/components/panels/AgentsPanel.tsx +22 -128
- package/app/components/panels/AgentsPanelAgentDetail.tsx +7 -6
- package/app/components/panels/AgentsPanelAgentGroups.tsx +8 -13
- package/app/components/panels/AgentsPanelAgentListRow.tsx +39 -16
- package/app/components/panels/AgentsPanelHubNav.tsx +12 -12
- package/app/components/panels/EchoPanel.tsx +8 -10
- package/app/components/panels/PanelNavRow.tsx +9 -2
- package/app/components/panels/PluginsPanel.tsx +5 -5
- package/app/components/renderers/agent-inspector/AgentInspectorRenderer.tsx +30 -8
- package/app/components/renderers/agent-inspector/manifest.ts +5 -3
- package/app/components/renderers/config/manifest.ts +1 -0
- package/app/components/renderers/csv/manifest.ts +1 -0
- package/app/components/renderers/todo/manifest.ts +1 -0
- package/app/components/settings/AiTab.tsx +3 -3
- package/app/components/settings/AppearanceTab.tsx +2 -2
- package/app/components/settings/KnowledgeTab.tsx +3 -3
- package/app/components/settings/McpAgentInstall.tsx +3 -6
- package/app/components/settings/McpSkillCreateForm.tsx +2 -3
- package/app/components/settings/McpSkillRow.tsx +2 -3
- package/app/components/settings/McpSkillsSection.tsx +2 -2
- package/app/components/settings/McpTab.tsx +12 -13
- package/app/components/settings/MonitoringTab.tsx +13 -13
- package/app/components/settings/PluginsTab.tsx +6 -5
- package/app/components/settings/Primitives.tsx +3 -4
- package/app/components/settings/SettingsContent.tsx +3 -3
- package/app/components/settings/SyncTab.tsx +11 -17
- package/app/components/settings/UpdateTab.tsx +18 -21
- package/app/components/settings/types.ts +14 -0
- package/app/components/setup/StepKB.tsx +1 -1
- package/app/hooks/useMcpData.tsx +7 -4
- package/app/hooks/useMention.ts +25 -8
- package/app/lib/agent/log.ts +15 -18
- package/app/lib/agent/stream-consumer.ts +3 -0
- package/app/lib/agent/to-agent-messages.ts +6 -4
- package/app/lib/core/agent-audit-log.ts +280 -0
- package/app/lib/core/content-changes.ts +148 -8
- package/app/lib/core/index.ts +11 -0
- package/app/lib/fs.ts +16 -1
- package/app/lib/i18n-en.ts +317 -36
- package/app/lib/i18n-zh.ts +316 -35
- package/app/lib/mcp-agents.ts +273 -2
- package/app/lib/renderers/index.ts +1 -2
- package/app/lib/renderers/registry.ts +10 -0
- package/app/lib/types.ts +2 -0
- package/app/next-env.d.ts +1 -1
- package/bin/lib/mcp-agents.js +38 -13
- package/package.json +1 -1
- package/scripts/migrate-agent-audit-log.js +170 -0
- package/scripts/migrate-agent-diff.js +146 -0
- package/scripts/setup.js +12 -17
- package/skills/plugin-core-builtin-migration/SKILL.md +178 -0
- package/app/components/renderers/diff/DiffRenderer.tsx +0 -311
- package/app/components/renderers/diff/manifest.ts +0 -14
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
|
4
4
|
import Link from 'next/link';
|
|
5
5
|
import { ChevronDown, ChevronRight, History, RefreshCw } from 'lucide-react';
|
|
6
6
|
import { apiFetch } from '@/lib/api';
|
|
7
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
7
8
|
import { collapseDiffContext, buildLineDiff } from './line-diff';
|
|
8
9
|
|
|
9
10
|
interface ChangeEvent {
|
|
@@ -28,18 +29,22 @@ interface ListPayload {
|
|
|
28
29
|
events: ChangeEvent[];
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function relativeTime(ts: string): string {
|
|
32
|
+
function relativeTime(ts: string, t: ReturnType<typeof useLocale>['t']): string {
|
|
32
33
|
const delta = Date.now() - new Date(ts).getTime();
|
|
33
34
|
const mins = Math.floor(delta / 60000);
|
|
34
|
-
if (mins < 1) return
|
|
35
|
-
if (mins < 60) return
|
|
35
|
+
if (mins < 1) return t.changes.relativeTime.justNow;
|
|
36
|
+
if (mins < 60) return t.changes.relativeTime.minutesAgo(mins);
|
|
36
37
|
const hours = Math.floor(mins / 60);
|
|
37
|
-
if (hours < 24) return
|
|
38
|
-
return
|
|
38
|
+
if (hours < 24) return t.changes.relativeTime.hoursAgo(hours);
|
|
39
|
+
return t.changes.relativeTime.daysAgo(Math.floor(hours / 24));
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export default function ChangesContentPage({ initialPath = '' }: { initialPath?: string }) {
|
|
43
|
+
const { t } = useLocale();
|
|
42
44
|
const [pathFilter, setPathFilter] = useState(initialPath);
|
|
45
|
+
const [sourceFilter, setSourceFilter] = useState<'all' | 'agent' | 'user' | 'system'>('all');
|
|
46
|
+
const [opFilter, setOpFilter] = useState<string>('all');
|
|
47
|
+
const [queryFilter, setQueryFilter] = useState('');
|
|
43
48
|
const [events, setEvents] = useState<ChangeEvent[]>([]);
|
|
44
49
|
const [summary, setSummary] = useState<SummaryPayload>({ unreadCount: 0, totalCount: 0 });
|
|
45
50
|
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
|
@@ -50,9 +55,12 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
50
55
|
setLoading(true);
|
|
51
56
|
setError(null);
|
|
52
57
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
const params = new URLSearchParams({ op: 'list', limit: '120' });
|
|
59
|
+
if (pathFilter.trim()) params.set('path', pathFilter.trim());
|
|
60
|
+
if (sourceFilter !== 'all') params.set('source', sourceFilter);
|
|
61
|
+
if (opFilter !== 'all') params.set('event_op', opFilter);
|
|
62
|
+
if (queryFilter.trim()) params.set('q', queryFilter.trim());
|
|
63
|
+
const listUrl = `/api/changes?${params.toString()}`;
|
|
56
64
|
const [list, summaryData] = await Promise.all([
|
|
57
65
|
apiFetch<ListPayload>(listUrl),
|
|
58
66
|
apiFetch<SummaryPayload>('/api/changes?op=summary'),
|
|
@@ -64,7 +72,7 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
64
72
|
} finally {
|
|
65
73
|
setLoading(false);
|
|
66
74
|
}
|
|
67
|
-
}, [pathFilter]);
|
|
75
|
+
}, [pathFilter, sourceFilter, opFilter, queryFilter]);
|
|
68
76
|
|
|
69
77
|
useEffect(() => {
|
|
70
78
|
void fetchData();
|
|
@@ -79,55 +87,116 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
79
87
|
await fetchData();
|
|
80
88
|
}, [fetchData]);
|
|
81
89
|
|
|
82
|
-
const eventCountLabel = useMemo(() =>
|
|
90
|
+
const eventCountLabel = useMemo(() => t.changes.eventsCount(events.length), [events.length, t]);
|
|
91
|
+
const opOptions = useMemo(() => {
|
|
92
|
+
const ops = Array.from(new Set(events.map((e) => e.op))).sort((a, b) => a.localeCompare(b));
|
|
93
|
+
if (opFilter !== 'all' && !ops.includes(opFilter)) ops.unshift(opFilter);
|
|
94
|
+
return ['all', ...ops];
|
|
95
|
+
}, [events, opFilter]);
|
|
96
|
+
|
|
97
|
+
const sourceLabel = useCallback((source: ChangeEvent['source']) => {
|
|
98
|
+
if (source === 'agent') return t.changes.filters.agent;
|
|
99
|
+
if (source === 'user') return t.changes.filters.user;
|
|
100
|
+
return t.changes.filters.system;
|
|
101
|
+
}, [t]);
|
|
83
102
|
|
|
84
103
|
return (
|
|
85
104
|
<div className="min-h-screen">
|
|
86
|
-
<div className="
|
|
87
|
-
<div className="content-width xl:mr-[220px]
|
|
88
|
-
<div className="
|
|
89
|
-
<div className="
|
|
90
|
-
<
|
|
91
|
-
|
|
105
|
+
<div className="px-4 md:px-6 pt-6 md:pt-8">
|
|
106
|
+
<div className="content-width xl:mr-[220px] rounded-xl border border-border bg-card px-4 py-3 md:px-5 md:py-4">
|
|
107
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
108
|
+
<div className="min-w-0">
|
|
109
|
+
<div className="flex items-center gap-2 text-sm font-medium text-foreground font-display">
|
|
110
|
+
<History size={15} />
|
|
111
|
+
{t.changes.title}
|
|
112
|
+
</div>
|
|
113
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
114
|
+
{t.changes.subtitle}
|
|
115
|
+
</p>
|
|
116
|
+
<div className="mt-2 flex items-center gap-2 text-xs">
|
|
117
|
+
<span className="rounded-full bg-muted px-2 py-0.5 text-muted-foreground">{eventCountLabel}</span>
|
|
118
|
+
<span className="rounded-full bg-[var(--amber-dim)] px-2 py-0.5 text-[var(--amber)]">
|
|
119
|
+
{t.changes.unreadCount(summary.unreadCount)}
|
|
120
|
+
</span>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div className="flex items-center gap-2">
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
onClick={() => void fetchData()}
|
|
127
|
+
className="px-2.5 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
128
|
+
>
|
|
129
|
+
<span className="inline-flex items-center gap-1"><RefreshCw size={12} /> {t.changes.refresh}</span>
|
|
130
|
+
</button>
|
|
131
|
+
<button
|
|
132
|
+
type="button"
|
|
133
|
+
onClick={() => void markSeen()}
|
|
134
|
+
className="px-2.5 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
135
|
+
>
|
|
136
|
+
{t.changes.markAllRead}
|
|
137
|
+
</button>
|
|
92
138
|
</div>
|
|
93
|
-
<div className="text-xs text-muted-foreground mt-1">{eventCountLabel} · {summary.unreadCount} unread</div>
|
|
94
|
-
</div>
|
|
95
|
-
<div className="flex items-center gap-2">
|
|
96
|
-
<button
|
|
97
|
-
type="button"
|
|
98
|
-
onClick={() => void fetchData()}
|
|
99
|
-
className="px-2.5 py-1.5 rounded-md text-xs font-medium bg-muted text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
100
|
-
>
|
|
101
|
-
<span className="inline-flex items-center gap-1"><RefreshCw size={12} /> Refresh</span>
|
|
102
|
-
</button>
|
|
103
|
-
<button
|
|
104
|
-
type="button"
|
|
105
|
-
onClick={() => void markSeen()}
|
|
106
|
-
className="px-2.5 py-1.5 rounded-md text-xs font-medium bg-[var(--amber)] text-[var(--amber-foreground)] focus-visible:ring-2 focus-visible:ring-ring"
|
|
107
|
-
>
|
|
108
|
-
Mark seen
|
|
109
|
-
</button>
|
|
110
139
|
</div>
|
|
111
140
|
</div>
|
|
112
141
|
</div>
|
|
113
142
|
|
|
114
|
-
<div className="px-4 md:px-6 py-
|
|
143
|
+
<div className="px-4 md:px-6 py-4 md:py-6">
|
|
115
144
|
<div className="content-width xl:mr-[220px] space-y-3">
|
|
116
145
|
<div className="rounded-lg border border-border bg-card p-3">
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
146
|
+
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-2.5">
|
|
147
|
+
<label className="block">
|
|
148
|
+
<span className="text-xs text-muted-foreground font-display">{t.changes.filters.filePath}</span>
|
|
149
|
+
<input
|
|
150
|
+
value={pathFilter}
|
|
151
|
+
onChange={(e) => setPathFilter(e.target.value)}
|
|
152
|
+
placeholder={t.changes.filters.filePathPlaceholder}
|
|
153
|
+
className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
154
|
+
/>
|
|
155
|
+
</label>
|
|
156
|
+
<label className="block">
|
|
157
|
+
<span className="text-xs text-muted-foreground font-display">{t.changes.filters.source}</span>
|
|
158
|
+
<select
|
|
159
|
+
value={sourceFilter}
|
|
160
|
+
onChange={(e) => setSourceFilter(e.target.value as 'all' | 'agent' | 'user' | 'system')}
|
|
161
|
+
className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
162
|
+
>
|
|
163
|
+
<option value="all">{t.changes.filters.all}</option>
|
|
164
|
+
<option value="agent">{t.changes.filters.agent}</option>
|
|
165
|
+
<option value="user">{t.changes.filters.user}</option>
|
|
166
|
+
<option value="system">{t.changes.filters.system}</option>
|
|
167
|
+
</select>
|
|
168
|
+
</label>
|
|
169
|
+
<label className="block">
|
|
170
|
+
<span className="text-xs text-muted-foreground font-display">{t.changes.filters.operation}</span>
|
|
171
|
+
<select
|
|
172
|
+
value={opFilter}
|
|
173
|
+
onChange={(e) => setOpFilter(e.target.value)}
|
|
174
|
+
className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
175
|
+
>
|
|
176
|
+
{opOptions.map((op) => (
|
|
177
|
+
<option key={op} value={op}>
|
|
178
|
+
{op === 'all' ? t.changes.filters.operationAll : op}
|
|
179
|
+
</option>
|
|
180
|
+
))}
|
|
181
|
+
</select>
|
|
182
|
+
</label>
|
|
183
|
+
<label className="block">
|
|
184
|
+
<span className="text-xs text-muted-foreground font-display">{t.changes.filters.keyword}</span>
|
|
185
|
+
<input
|
|
186
|
+
value={queryFilter}
|
|
187
|
+
onChange={(e) => setQueryFilter(e.target.value)}
|
|
188
|
+
placeholder={t.changes.filters.keywordPlaceholder}
|
|
189
|
+
className="mt-1 w-full px-2.5 py-1.5 text-sm bg-background border border-border rounded-lg text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
190
|
+
/>
|
|
191
|
+
</label>
|
|
192
|
+
</div>
|
|
124
193
|
</div>
|
|
125
194
|
|
|
126
|
-
{loading && <p className="text-sm text-muted-foreground">
|
|
195
|
+
{loading && <p className="text-sm text-muted-foreground">{t.changes.loading}</p>}
|
|
127
196
|
{error && <p className="text-sm text-error">{error}</p>}
|
|
128
197
|
{!loading && !error && events.length === 0 && (
|
|
129
198
|
<div className="rounded-lg border border-border bg-card p-6 text-center text-sm text-muted-foreground">
|
|
130
|
-
|
|
199
|
+
{t.changes.empty}
|
|
131
200
|
</div>
|
|
132
201
|
)}
|
|
133
202
|
|
|
@@ -135,35 +204,49 @@ export default function ChangesContentPage({ initialPath = '' }: { initialPath?:
|
|
|
135
204
|
const open = !!expanded[event.id];
|
|
136
205
|
const rows = collapseDiffContext(buildLineDiff(event.before ?? '', event.after ?? ''));
|
|
137
206
|
return (
|
|
138
|
-
<div key={event.id} className="rounded-
|
|
207
|
+
<div key={event.id} className="rounded-xl border border-border bg-card overflow-hidden">
|
|
139
208
|
<button
|
|
140
209
|
type="button"
|
|
141
210
|
onClick={() => setExpanded((prev) => ({ ...prev, [event.id]: !prev[event.id] }))}
|
|
142
|
-
className="w-full px-3 py-
|
|
211
|
+
className="w-full px-3 py-3 text-left hover:bg-muted/30 focus-visible:ring-2 focus-visible:ring-ring"
|
|
143
212
|
>
|
|
144
213
|
<div className="flex items-start gap-2">
|
|
145
214
|
<span className="pt-0.5 text-muted-foreground">{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}</span>
|
|
146
215
|
<div className="min-w-0 flex-1">
|
|
147
216
|
<div className="text-sm font-medium text-foreground font-display">{event.summary}</div>
|
|
148
|
-
<div className="text-xs text-muted-foreground
|
|
149
|
-
|
|
217
|
+
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground">
|
|
218
|
+
<span
|
|
219
|
+
className="rounded-md px-2 py-0.5 font-medium"
|
|
220
|
+
style={{
|
|
221
|
+
background: 'color-mix(in srgb, var(--amber) 16%, var(--muted))',
|
|
222
|
+
color: 'var(--foreground)',
|
|
223
|
+
border: '1px solid color-mix(in srgb, var(--amber) 36%, var(--border))',
|
|
224
|
+
}}
|
|
225
|
+
>
|
|
226
|
+
{event.path}
|
|
227
|
+
</span>
|
|
228
|
+
<span>{event.op}</span>
|
|
229
|
+
<span>·</span>
|
|
230
|
+
<span>{sourceLabel(event.source)}</span>
|
|
231
|
+
<span>·</span>
|
|
232
|
+
<span>{relativeTime(event.ts, t)}</span>
|
|
150
233
|
</div>
|
|
151
234
|
</div>
|
|
152
235
|
<Link
|
|
153
236
|
href={`/view/${event.path.split('/').map(encodeURIComponent).join('/')}`}
|
|
154
|
-
className="text-xs text-[var(--amber)] hover:
|
|
237
|
+
className="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium bg-[var(--amber-dim)] text-[var(--amber)] focus-visible:ring-2 focus-visible:ring-ring hover:opacity-90"
|
|
155
238
|
onClick={(e) => e.stopPropagation()}
|
|
156
239
|
>
|
|
157
|
-
|
|
240
|
+
{t.changes.open}
|
|
158
241
|
</Link>
|
|
159
242
|
</div>
|
|
160
243
|
</button>
|
|
161
244
|
|
|
162
245
|
{open && (
|
|
163
|
-
<div className="border-t border-border bg-background">
|
|
246
|
+
<div className="border-t border-border bg-background/70">
|
|
164
247
|
{rows.map((row, idx) => {
|
|
165
248
|
if (row.type === 'gap') {
|
|
166
|
-
return <div key={`${event.id}-gap-${idx}`} className="px-3 py-1 text-2xs text-muted-foreground"
|
|
249
|
+
return <div key={`${event.id}-gap-${idx}`} className="px-3 py-1 text-2xs text-muted-foreground">{t.changes.unchangedLines(row.count)}</div>;
|
|
167
250
|
}
|
|
168
251
|
const prefix = row.type === 'insert' ? '+' : row.type === 'delete' ? '-' : ' ';
|
|
169
252
|
const color = row.type === 'insert'
|
|
@@ -1,55 +1,41 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Echo page hero: kicker,
|
|
7
|
-
*
|
|
6
|
+
* Echo page hero: kicker, h1, lead, and optional embedded children (e.g. segment nav).
|
|
7
|
+
* The accent bar highlights the text zone; children sit below it inside the card.
|
|
8
8
|
*/
|
|
9
9
|
export function EchoHero({
|
|
10
|
-
breadcrumbNav,
|
|
11
|
-
parentHref,
|
|
12
|
-
parent,
|
|
13
10
|
heroKicker,
|
|
14
11
|
pageTitle,
|
|
15
12
|
lead,
|
|
16
13
|
titleId,
|
|
14
|
+
children,
|
|
17
15
|
}: {
|
|
18
|
-
breadcrumbNav: string;
|
|
19
|
-
parentHref: string;
|
|
20
|
-
parent: string;
|
|
21
16
|
heroKicker: string;
|
|
22
17
|
pageTitle: string;
|
|
23
18
|
lead: string;
|
|
24
19
|
titleId: string;
|
|
20
|
+
children?: ReactNode;
|
|
25
21
|
}) {
|
|
26
22
|
return (
|
|
27
|
-
<header className="relative overflow-hidden rounded-xl border border-border bg-card px-5
|
|
23
|
+
<header className="relative overflow-hidden rounded-xl border border-border bg-card px-5 pb-5 pt-6 shadow-sm sm:px-8 sm:pb-6 sm:pt-8">
|
|
28
24
|
<div
|
|
29
|
-
className="absolute
|
|
25
|
+
className="absolute left-0 top-5 w-[3px] rounded-full bg-[var(--amber)] sm:top-6"
|
|
26
|
+
style={{ bottom: children ? '40%' : '1.25rem' }}
|
|
30
27
|
aria-hidden
|
|
31
28
|
/>
|
|
32
29
|
<div className="relative pl-4 sm:pl-5">
|
|
33
|
-
<p className="mb-
|
|
30
|
+
<p className="mb-4 font-sans text-2xs font-semibold uppercase tracking-[0.2em] text-[var(--amber)]">
|
|
34
31
|
{heroKicker}
|
|
35
32
|
</p>
|
|
36
|
-
<nav aria-label={breadcrumbNav} className="mb-5 font-sans text-sm">
|
|
37
|
-
<ol className="m-0 list-none p-0">
|
|
38
|
-
<li>
|
|
39
|
-
<Link
|
|
40
|
-
href={parentHref}
|
|
41
|
-
className="text-muted-foreground transition-colors duration-150 hover:text-[var(--amber)] focus-visible:rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
42
|
-
>
|
|
43
|
-
{parent}
|
|
44
|
-
</Link>
|
|
45
|
-
</li>
|
|
46
|
-
</ol>
|
|
47
|
-
</nav>
|
|
48
33
|
<h1 id={titleId} className="font-display text-2xl font-semibold tracking-tight text-foreground md:text-3xl">
|
|
49
34
|
{pageTitle}
|
|
50
35
|
</h1>
|
|
51
36
|
<p className="mt-3 max-w-prose font-sans text-base leading-relaxed text-muted-foreground">{lead}</p>
|
|
52
37
|
</div>
|
|
38
|
+
{children}
|
|
53
39
|
</header>
|
|
54
40
|
);
|
|
55
41
|
}
|
|
@@ -103,7 +103,7 @@ export function EchoInsightCollapsible({
|
|
|
103
103
|
const generateDisabled = aiLoading || !aiReady || streaming;
|
|
104
104
|
|
|
105
105
|
return (
|
|
106
|
-
<div className="mt-10 overflow-hidden rounded-xl border border-border bg-card shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--amber)]/15 hover:shadow
|
|
106
|
+
<div className="mt-10 overflow-hidden rounded-xl border border-border bg-card shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--amber)]/15 hover:shadow">
|
|
107
107
|
<button
|
|
108
108
|
id={btnId}
|
|
109
109
|
type="button"
|
|
@@ -129,56 +129,65 @@ export function EchoInsightCollapsible({
|
|
|
129
129
|
/>
|
|
130
130
|
<span className="sr-only">{open ? hideLabel : showLabel}</span>
|
|
131
131
|
</button>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
className="inline-flex items-center gap-2 rounded-lg bg-[var(--amber)] px-3 py-2 font-sans text-sm font-medium text-white transition-opacity duration-150 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
146
|
-
>
|
|
147
|
-
{streaming ? <Loader2 size={16} className="animate-spin shrink-0" aria-hidden /> : null}
|
|
148
|
-
{streaming ? generatingLabel : generateLabel}
|
|
149
|
-
</button>
|
|
150
|
-
{err ? (
|
|
132
|
+
<div
|
|
133
|
+
id={panelId}
|
|
134
|
+
role="region"
|
|
135
|
+
aria-labelledby={btnId}
|
|
136
|
+
className={cn(
|
|
137
|
+
'grid transition-[grid-template-rows] duration-250 ease-out',
|
|
138
|
+
open ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
|
|
139
|
+
)}
|
|
140
|
+
>
|
|
141
|
+
<div className="overflow-hidden">
|
|
142
|
+
<div className="border-t border-border/60 px-5 pb-5 pt-4">
|
|
143
|
+
<p className="font-sans text-sm leading-relaxed text-muted-foreground">{hint}</p>
|
|
144
|
+
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
151
145
|
<button
|
|
152
146
|
type="button"
|
|
147
|
+
disabled={generateDisabled}
|
|
153
148
|
onClick={runGenerate}
|
|
154
|
-
|
|
155
|
-
className="font-sans text-sm text-[var(--amber)] underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
149
|
+
className="inline-flex items-center gap-2 rounded-lg bg-[var(--amber)] px-3 py-2 font-sans text-sm font-medium text-[var(--amber-foreground)] transition-opacity duration-150 hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
156
150
|
>
|
|
157
|
-
{
|
|
151
|
+
{streaming ? (
|
|
152
|
+
<Loader2 size={16} className="animate-spin shrink-0" aria-hidden />
|
|
153
|
+
) : (
|
|
154
|
+
<Sparkles size={15} className="shrink-0" aria-hidden />
|
|
155
|
+
)}
|
|
156
|
+
{streaming ? generatingLabel : generateLabel}
|
|
158
157
|
</button>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
) : null}
|
|
169
|
-
{insightMd ? (
|
|
170
|
-
<div className={cn(proseInsight, 'mt-4 border-t border-border/60 pt-4')}>
|
|
171
|
-
<ReactMarkdown remarkPlugins={[remarkGfm]}>{insightMd}</ReactMarkdown>
|
|
172
|
-
{streaming ? (
|
|
173
|
-
<span
|
|
174
|
-
className="ml-0.5 inline-block h-3.5 w-1 animate-pulse rounded-sm bg-[var(--amber)] align-middle"
|
|
175
|
-
aria-hidden
|
|
176
|
-
/>
|
|
158
|
+
{err ? (
|
|
159
|
+
<button
|
|
160
|
+
type="button"
|
|
161
|
+
onClick={runGenerate}
|
|
162
|
+
disabled={streaming || !aiReady}
|
|
163
|
+
className="font-sans text-sm text-[var(--amber)] underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-sm"
|
|
164
|
+
>
|
|
165
|
+
{retryLabel}
|
|
166
|
+
</button>
|
|
177
167
|
) : null}
|
|
178
168
|
</div>
|
|
179
|
-
|
|
169
|
+
{!aiLoading && !aiReady ? (
|
|
170
|
+
<p className="mt-2 font-sans text-2xs text-muted-foreground">{noAiHint}</p>
|
|
171
|
+
) : null}
|
|
172
|
+
{err ? (
|
|
173
|
+
<p className="mt-3 font-sans text-sm text-error" role="alert">
|
|
174
|
+
{errorPrefix} {err}
|
|
175
|
+
</p>
|
|
176
|
+
) : null}
|
|
177
|
+
{insightMd ? (
|
|
178
|
+
<div className={cn(proseInsight, 'mt-4 border-t border-border/60 pt-4')}>
|
|
179
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{insightMd}</ReactMarkdown>
|
|
180
|
+
{streaming ? (
|
|
181
|
+
<span
|
|
182
|
+
className="ml-0.5 inline-block h-3.5 w-1 animate-pulse rounded-sm bg-[var(--amber)] align-middle"
|
|
183
|
+
aria-hidden
|
|
184
|
+
/>
|
|
185
|
+
) : null}
|
|
186
|
+
</div>
|
|
187
|
+
) : null}
|
|
188
|
+
</div>
|
|
180
189
|
</div>
|
|
181
|
-
|
|
190
|
+
</div>
|
|
182
191
|
</div>
|
|
183
192
|
);
|
|
184
193
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react';
|
|
4
|
-
import { Library } from 'lucide-react';
|
|
4
|
+
import { Library, FileText, CircleCheck } from 'lucide-react';
|
|
5
5
|
|
|
6
6
|
export function EchoFactSnapshot({
|
|
7
7
|
headingId,
|
|
@@ -9,6 +9,7 @@ export function EchoFactSnapshot({
|
|
|
9
9
|
snapshotBadge,
|
|
10
10
|
emptyTitle,
|
|
11
11
|
emptyBody,
|
|
12
|
+
icon,
|
|
12
13
|
actions,
|
|
13
14
|
}: {
|
|
14
15
|
headingId: string;
|
|
@@ -16,12 +17,12 @@ export function EchoFactSnapshot({
|
|
|
16
17
|
snapshotBadge: string;
|
|
17
18
|
emptyTitle: string;
|
|
18
19
|
emptyBody: string;
|
|
19
|
-
|
|
20
|
+
icon?: ReactNode;
|
|
20
21
|
actions?: ReactNode;
|
|
21
22
|
}) {
|
|
22
23
|
return (
|
|
23
24
|
<section
|
|
24
|
-
className="rounded-xl border border-border bg-card p-5 shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--amber)]/20 hover:shadow
|
|
25
|
+
className="rounded-xl border border-border bg-card p-5 shadow-sm transition-[border-color,box-shadow] duration-150 ease-out hover:border-[var(--amber)]/20 hover:shadow sm:p-6"
|
|
25
26
|
aria-labelledby={headingId}
|
|
26
27
|
>
|
|
27
28
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
@@ -30,7 +31,7 @@ export function EchoFactSnapshot({
|
|
|
30
31
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--amber-dim)] text-[var(--amber)]"
|
|
31
32
|
aria-hidden
|
|
32
33
|
>
|
|
33
|
-
<Library size={18} strokeWidth={1.75} />
|
|
34
|
+
{icon ?? <Library size={18} strokeWidth={1.75} />}
|
|
34
35
|
</span>
|
|
35
36
|
<div>
|
|
36
37
|
<h2
|
|
@@ -67,9 +68,12 @@ export function EchoContinuedGroups({
|
|
|
67
68
|
subEmptyHint: string;
|
|
68
69
|
footer?: ReactNode;
|
|
69
70
|
}) {
|
|
70
|
-
const cell = (label: string) => (
|
|
71
|
-
<div className="flex min-h-[5.75rem] flex-col justify-center rounded-xl border border-dashed border-border/80 bg-muted/10 px-4 py-4">
|
|
72
|
-
<
|
|
71
|
+
const cell = (label: string, icon: ReactNode) => (
|
|
72
|
+
<div className="flex min-h-[5.75rem] flex-col justify-center rounded-xl border border-dashed border-border/80 bg-muted/10 px-4 py-4 transition-colors duration-150 hover:border-[var(--amber)]/25 hover:bg-[var(--amber-dim)]/15">
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
<span className="shrink-0 text-muted-foreground" aria-hidden>{icon}</span>
|
|
75
|
+
<h3 className="font-sans text-sm font-medium text-foreground">{label}</h3>
|
|
76
|
+
</div>
|
|
73
77
|
<p className="mt-2 font-sans text-2xs leading-relaxed text-muted-foreground">{subEmptyHint}</p>
|
|
74
78
|
</div>
|
|
75
79
|
);
|
|
@@ -77,8 +81,8 @@ export function EchoContinuedGroups({
|
|
|
77
81
|
return (
|
|
78
82
|
<div className="space-y-4">
|
|
79
83
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
80
|
-
{cell(draftsLabel)}
|
|
81
|
-
{cell(todosLabel)}
|
|
84
|
+
{cell(draftsLabel, <FileText size={15} strokeWidth={1.75} />)}
|
|
85
|
+
{cell(todosLabel, <CircleCheck size={15} strokeWidth={1.75} />)}
|
|
82
86
|
</div>
|
|
83
87
|
{footer ? <div className="border-t border-border/60 pt-4">{footer}</div> : null}
|
|
84
88
|
</div>
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
3
4
|
import Link from 'next/link';
|
|
5
|
+
import { UserRound, Bookmark, Sun, History, Brain } from 'lucide-react';
|
|
4
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
5
7
|
import { cn } from '@/lib/utils';
|
|
6
8
|
import { ECHO_SEGMENT_HREF, ECHO_SEGMENT_ORDER, type EchoSegment } from '@/lib/echo-segments';
|
|
7
9
|
|
|
8
|
-
function
|
|
10
|
+
function segmentMeta(
|
|
9
11
|
segment: EchoSegment,
|
|
10
12
|
echo: ReturnType<typeof useLocale>['t']['panels']['echo'],
|
|
11
|
-
): string {
|
|
13
|
+
): { label: string; icon: ReactNode } {
|
|
12
14
|
switch (segment) {
|
|
13
15
|
case 'about-you':
|
|
14
|
-
return echo.aboutYouTitle;
|
|
16
|
+
return { label: echo.aboutYouTitle, icon: <UserRound size={13} /> };
|
|
15
17
|
case 'continued':
|
|
16
|
-
return echo.continuedTitle;
|
|
18
|
+
return { label: echo.continuedTitle, icon: <Bookmark size={13} /> };
|
|
17
19
|
case 'daily':
|
|
18
|
-
return echo.dailyEchoTitle;
|
|
20
|
+
return { label: echo.dailyEchoTitle, icon: <Sun size={13} /> };
|
|
19
21
|
case 'past-you':
|
|
20
|
-
return echo.pastYouTitle;
|
|
22
|
+
return { label: echo.pastYouTitle, icon: <History size={13} /> };
|
|
21
23
|
case 'growth':
|
|
22
|
-
return echo.intentGrowthTitle;
|
|
24
|
+
return { label: echo.intentGrowthTitle, icon: <Brain size={13} /> };
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -29,11 +31,11 @@ export default function EchoSegmentNav({ activeSegment }: { activeSegment: EchoS
|
|
|
29
31
|
const aria = t.echoPages.segmentNavAria;
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
|
-
<nav aria-label={aria} className="mt-
|
|
33
|
-
<ul className="
|
|
34
|
+
<nav aria-label={aria} className="mt-5 border-t border-border/30 pt-4 font-sans">
|
|
35
|
+
<ul className="flex snap-x snap-mandatory gap-1.5 overflow-x-auto pb-0.5 [scrollbar-width:thin]">
|
|
34
36
|
{ECHO_SEGMENT_ORDER.map((segment) => {
|
|
35
37
|
const href = ECHO_SEGMENT_HREF[segment];
|
|
36
|
-
const label =
|
|
38
|
+
const { label, icon } = segmentMeta(segment, echo);
|
|
37
39
|
const isActive = segment === activeSegment;
|
|
38
40
|
return (
|
|
39
41
|
<li key={segment} className="snap-start shrink-0">
|
|
@@ -41,12 +43,13 @@ export default function EchoSegmentNav({ activeSegment }: { activeSegment: EchoS
|
|
|
41
43
|
href={href}
|
|
42
44
|
aria-current={isActive ? 'page' : undefined}
|
|
43
45
|
className={cn(
|
|
44
|
-
'inline-flex min-h-9 max-w-[11rem] items-center rounded-full border px-3 py-1.5 text-sm transition-[background-color,border-color,color] duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
46
|
+
'inline-flex min-h-9 max-w-[11rem] items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm transition-[background-color,border-color,color] duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
45
47
|
isActive
|
|
46
48
|
? 'border-[var(--amber)]/45 bg-[var(--amber-dim)]/50 font-medium text-foreground'
|
|
47
49
|
: 'border-transparent bg-muted/35 text-muted-foreground hover:bg-muted/55 hover:text-foreground',
|
|
48
50
|
)}
|
|
49
51
|
>
|
|
52
|
+
<span className="shrink-0" aria-hidden>{icon}</span>
|
|
50
53
|
<span className="truncate">{label}</span>
|
|
51
54
|
</Link>
|
|
52
55
|
</li>
|