@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,184 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
4
|
+
import { ChevronDown, Loader2, Sparkles } from 'lucide-react';
|
|
5
|
+
import ReactMarkdown from 'react-markdown';
|
|
6
|
+
import remarkGfm from 'remark-gfm';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
import { consumeUIMessageStream } from '@/lib/agent/stream-consumer';
|
|
9
|
+
import { useSettingsAiAvailable } from '@/hooks/useSettingsAiAvailable';
|
|
10
|
+
|
|
11
|
+
const proseInsight =
|
|
12
|
+
'prose prose-sm prose-panel dark:prose-invert max-w-none text-foreground ' +
|
|
13
|
+
'prose-p:my-1 prose-p:leading-relaxed ' +
|
|
14
|
+
'prose-headings:font-semibold prose-headings:my-2 prose-headings:text-[13px] ' +
|
|
15
|
+
'prose-ul:my-1 prose-li:my-0.5 prose-ol:my-1 ' +
|
|
16
|
+
'prose-code:text-[0.8em] prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none ' +
|
|
17
|
+
'prose-pre:bg-muted prose-pre:text-foreground prose-pre:text-xs ' +
|
|
18
|
+
'prose-blockquote:border-l-[var(--amber)] prose-blockquote:text-muted-foreground ' +
|
|
19
|
+
'prose-a:text-[var(--amber)] prose-a:no-underline hover:prose-a:underline ' +
|
|
20
|
+
'prose-strong:text-foreground prose-strong:font-semibold';
|
|
21
|
+
|
|
22
|
+
export function EchoInsightCollapsible({
|
|
23
|
+
title,
|
|
24
|
+
showLabel,
|
|
25
|
+
hideLabel,
|
|
26
|
+
hint,
|
|
27
|
+
generateLabel,
|
|
28
|
+
noAiHint,
|
|
29
|
+
generatingLabel,
|
|
30
|
+
errorPrefix,
|
|
31
|
+
retryLabel,
|
|
32
|
+
userPrompt,
|
|
33
|
+
}: {
|
|
34
|
+
title: string;
|
|
35
|
+
showLabel: string;
|
|
36
|
+
hideLabel: string;
|
|
37
|
+
hint: string;
|
|
38
|
+
generateLabel: string;
|
|
39
|
+
noAiHint: string;
|
|
40
|
+
generatingLabel: string;
|
|
41
|
+
errorPrefix: string;
|
|
42
|
+
retryLabel: string;
|
|
43
|
+
userPrompt: string;
|
|
44
|
+
}) {
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
const [streaming, setStreaming] = useState(false);
|
|
47
|
+
const [insightMd, setInsightMd] = useState('');
|
|
48
|
+
const [err, setErr] = useState('');
|
|
49
|
+
const panelId = useId();
|
|
50
|
+
const btnId = `${panelId}-btn`;
|
|
51
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
52
|
+
const { ready: aiReady, loading: aiLoading } = useSettingsAiAvailable();
|
|
53
|
+
|
|
54
|
+
useEffect(() => () => abortRef.current?.abort(), []);
|
|
55
|
+
|
|
56
|
+
const runGenerate = useCallback(async () => {
|
|
57
|
+
abortRef.current?.abort();
|
|
58
|
+
const ctrl = new AbortController();
|
|
59
|
+
abortRef.current = ctrl;
|
|
60
|
+
setErr('');
|
|
61
|
+
setInsightMd('');
|
|
62
|
+
setStreaming(true);
|
|
63
|
+
try {
|
|
64
|
+
const res = await fetch('/api/ask', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
69
|
+
maxSteps: 16,
|
|
70
|
+
}),
|
|
71
|
+
signal: ctrl.signal,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
let msg = `HTTP ${res.status}`;
|
|
76
|
+
try {
|
|
77
|
+
const j = (await res.json()) as { error?: { message?: string }; message?: string };
|
|
78
|
+
msg = j?.error?.message ?? j?.message ?? msg;
|
|
79
|
+
} catch {
|
|
80
|
+
/* ignore */
|
|
81
|
+
}
|
|
82
|
+
throw new Error(msg);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!res.body) throw new Error('No response body');
|
|
86
|
+
|
|
87
|
+
await consumeUIMessageStream(
|
|
88
|
+
res.body,
|
|
89
|
+
(msg) => {
|
|
90
|
+
setInsightMd(msg.content ?? '');
|
|
91
|
+
},
|
|
92
|
+
ctrl.signal,
|
|
93
|
+
);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (e instanceof Error && e.name === 'AbortError') return;
|
|
96
|
+
setErr(e instanceof Error ? e.message : String(e));
|
|
97
|
+
} finally {
|
|
98
|
+
setStreaming(false);
|
|
99
|
+
abortRef.current = null;
|
|
100
|
+
}
|
|
101
|
+
}, [userPrompt]);
|
|
102
|
+
|
|
103
|
+
const generateDisabled = aiLoading || !aiReady || streaming;
|
|
104
|
+
|
|
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-md">
|
|
107
|
+
<button
|
|
108
|
+
id={btnId}
|
|
109
|
+
type="button"
|
|
110
|
+
className="flex w-full items-center gap-3 px-5 py-4 text-left transition-colors duration-200 hover:bg-muted/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
|
|
111
|
+
aria-expanded={open}
|
|
112
|
+
aria-controls={panelId}
|
|
113
|
+
onClick={() => setOpen((v) => !v)}
|
|
114
|
+
>
|
|
115
|
+
<span
|
|
116
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--amber-dim)] text-[var(--amber)]"
|
|
117
|
+
aria-hidden
|
|
118
|
+
>
|
|
119
|
+
<Sparkles size={17} strokeWidth={1.75} />
|
|
120
|
+
</span>
|
|
121
|
+
<span className="flex-1 font-sans text-sm font-medium text-foreground">{title}</span>
|
|
122
|
+
<ChevronDown
|
|
123
|
+
size={16}
|
|
124
|
+
className={cn(
|
|
125
|
+
'shrink-0 text-muted-foreground transition-transform duration-200',
|
|
126
|
+
open && 'rotate-180',
|
|
127
|
+
)}
|
|
128
|
+
aria-hidden
|
|
129
|
+
/>
|
|
130
|
+
<span className="sr-only">{open ? hideLabel : showLabel}</span>
|
|
131
|
+
</button>
|
|
132
|
+
{open ? (
|
|
133
|
+
<div
|
|
134
|
+
id={panelId}
|
|
135
|
+
role="region"
|
|
136
|
+
aria-labelledby={btnId}
|
|
137
|
+
className="border-t border-border/60 px-5 pb-5 pt-4"
|
|
138
|
+
>
|
|
139
|
+
<p className="font-sans text-sm leading-relaxed text-muted-foreground">{hint}</p>
|
|
140
|
+
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
disabled={generateDisabled}
|
|
144
|
+
onClick={runGenerate}
|
|
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 ? (
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={runGenerate}
|
|
154
|
+
disabled={streaming || !aiReady}
|
|
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"
|
|
156
|
+
>
|
|
157
|
+
{retryLabel}
|
|
158
|
+
</button>
|
|
159
|
+
) : null}
|
|
160
|
+
</div>
|
|
161
|
+
{!aiLoading && !aiReady ? (
|
|
162
|
+
<p className="mt-2 font-sans text-2xs text-muted-foreground">{noAiHint}</p>
|
|
163
|
+
) : null}
|
|
164
|
+
{err ? (
|
|
165
|
+
<p className="mt-3 font-sans text-sm text-error" role="alert">
|
|
166
|
+
{errorPrefix} {err}
|
|
167
|
+
</p>
|
|
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
|
+
/>
|
|
177
|
+
) : null}
|
|
178
|
+
</div>
|
|
179
|
+
) : null}
|
|
180
|
+
</div>
|
|
181
|
+
) : null}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { Library } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export function EchoFactSnapshot({
|
|
7
|
+
headingId,
|
|
8
|
+
heading,
|
|
9
|
+
snapshotBadge,
|
|
10
|
+
emptyTitle,
|
|
11
|
+
emptyBody,
|
|
12
|
+
actions,
|
|
13
|
+
}: {
|
|
14
|
+
headingId: string;
|
|
15
|
+
heading: string;
|
|
16
|
+
snapshotBadge: string;
|
|
17
|
+
emptyTitle: string;
|
|
18
|
+
emptyBody: string;
|
|
19
|
+
/** e.g. continue-in-Agent CTA — inside this card so it is not orphaned between sections */
|
|
20
|
+
actions?: ReactNode;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<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-md sm:p-6"
|
|
25
|
+
aria-labelledby={headingId}
|
|
26
|
+
>
|
|
27
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
28
|
+
<div className="flex items-start gap-3">
|
|
29
|
+
<span
|
|
30
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--amber-dim)] text-[var(--amber)]"
|
|
31
|
+
aria-hidden
|
|
32
|
+
>
|
|
33
|
+
<Library size={18} strokeWidth={1.75} />
|
|
34
|
+
</span>
|
|
35
|
+
<div>
|
|
36
|
+
<h2
|
|
37
|
+
id={headingId}
|
|
38
|
+
className="font-sans text-xs font-semibold uppercase tracking-wide text-muted-foreground"
|
|
39
|
+
>
|
|
40
|
+
{heading}
|
|
41
|
+
</h2>
|
|
42
|
+
<p className="mt-2 font-sans font-medium text-foreground">{emptyTitle}</p>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<span className="font-sans text-2xs font-medium uppercase tracking-wide text-[var(--amber)] sm:mt-0.5 sm:shrink-0 rounded-md bg-[var(--amber-dim)] px-2 py-1">
|
|
46
|
+
{snapshotBadge}
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
<p className="mt-4 border-t border-border/60 pt-4 font-sans text-sm leading-relaxed text-muted-foreground">
|
|
50
|
+
{emptyBody}
|
|
51
|
+
</p>
|
|
52
|
+
{actions ? (
|
|
53
|
+
<div className="mt-4 border-t border-border/60 pt-4">{actions}</div>
|
|
54
|
+
) : null}
|
|
55
|
+
</section>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function EchoContinuedGroups({
|
|
60
|
+
draftsLabel,
|
|
61
|
+
todosLabel,
|
|
62
|
+
subEmptyHint,
|
|
63
|
+
footer,
|
|
64
|
+
}: {
|
|
65
|
+
draftsLabel: string;
|
|
66
|
+
todosLabel: string;
|
|
67
|
+
subEmptyHint: string;
|
|
68
|
+
footer?: ReactNode;
|
|
69
|
+
}) {
|
|
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
|
+
<h3 className="font-sans text-sm font-medium text-foreground">{label}</h3>
|
|
73
|
+
<p className="mt-2 font-sans text-2xs leading-relaxed text-muted-foreground">{subEmptyHint}</p>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className="space-y-4">
|
|
79
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
80
|
+
{cell(draftsLabel)}
|
|
81
|
+
{cell(todosLabel)}
|
|
82
|
+
</div>
|
|
83
|
+
{footer ? <div className="border-t border-border/60 pt-4">{footer}</div> : null}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import { ECHO_SEGMENT_HREF, ECHO_SEGMENT_ORDER, type EchoSegment } from '@/lib/echo-segments';
|
|
7
|
+
|
|
8
|
+
function labelForSegment(
|
|
9
|
+
segment: EchoSegment,
|
|
10
|
+
echo: ReturnType<typeof useLocale>['t']['panels']['echo'],
|
|
11
|
+
): string {
|
|
12
|
+
switch (segment) {
|
|
13
|
+
case 'about-you':
|
|
14
|
+
return echo.aboutYouTitle;
|
|
15
|
+
case 'continued':
|
|
16
|
+
return echo.continuedTitle;
|
|
17
|
+
case 'daily':
|
|
18
|
+
return echo.dailyEchoTitle;
|
|
19
|
+
case 'past-you':
|
|
20
|
+
return echo.pastYouTitle;
|
|
21
|
+
case 'growth':
|
|
22
|
+
return echo.intentGrowthTitle;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function EchoSegmentNav({ activeSegment }: { activeSegment: EchoSegment }) {
|
|
27
|
+
const { t } = useLocale();
|
|
28
|
+
const echo = t.panels.echo;
|
|
29
|
+
const aria = t.echoPages.segmentNavAria;
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<nav aria-label={aria} className="mt-6 font-sans">
|
|
33
|
+
<ul className="-mx-1 flex snap-x snap-mandatory gap-1.5 overflow-x-auto px-1 pb-1 [scrollbar-width:thin]">
|
|
34
|
+
{ECHO_SEGMENT_ORDER.map((segment) => {
|
|
35
|
+
const href = ECHO_SEGMENT_HREF[segment];
|
|
36
|
+
const label = labelForSegment(segment, echo);
|
|
37
|
+
const isActive = segment === activeSegment;
|
|
38
|
+
return (
|
|
39
|
+
<li key={segment} className="snap-start shrink-0">
|
|
40
|
+
<Link
|
|
41
|
+
href={href}
|
|
42
|
+
aria-current={isActive ? 'page' : undefined}
|
|
43
|
+
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',
|
|
45
|
+
isActive
|
|
46
|
+
? 'border-[var(--amber)]/45 bg-[var(--amber-dim)]/50 font-medium text-foreground'
|
|
47
|
+
: 'border-transparent bg-muted/35 text-muted-foreground hover:bg-muted/55 hover:text-foreground',
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<span className="truncate">{label}</span>
|
|
51
|
+
</Link>
|
|
52
|
+
</li>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
</ul>
|
|
56
|
+
</nav>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useId, useMemo, useState } from 'react';
|
|
4
|
+
import type { EchoSegment } from '@/lib/echo-segments';
|
|
5
|
+
import { buildEchoInsightUserPrompt } from '@/lib/echo-insight-prompt';
|
|
6
|
+
import type { Locale } from '@/lib/i18n';
|
|
7
|
+
import { useLocale } from '@/lib/LocaleContext';
|
|
8
|
+
import { openAskModal } from '@/hooks/useAskModal';
|
|
9
|
+
import { EchoHero } from './EchoHero';
|
|
10
|
+
import EchoSegmentNav from './EchoSegmentNav';
|
|
11
|
+
import { EchoInsightCollapsible } from './EchoInsightCollapsible';
|
|
12
|
+
import { EchoContinuedGroups, EchoFactSnapshot } from './EchoPageSections';
|
|
13
|
+
|
|
14
|
+
const STORAGE_DAILY = 'mindos-echo-daily-line';
|
|
15
|
+
const STORAGE_GROWTH = 'mindos-echo-growth-intent';
|
|
16
|
+
|
|
17
|
+
function segmentTitle(segment: EchoSegment, echo: ReturnType<typeof useLocale>['t']['panels']['echo']): string {
|
|
18
|
+
switch (segment) {
|
|
19
|
+
case 'about-you':
|
|
20
|
+
return echo.aboutYouTitle;
|
|
21
|
+
case 'continued':
|
|
22
|
+
return echo.continuedTitle;
|
|
23
|
+
case 'daily':
|
|
24
|
+
return echo.dailyEchoTitle;
|
|
25
|
+
case 'past-you':
|
|
26
|
+
return echo.pastYouTitle;
|
|
27
|
+
case 'growth':
|
|
28
|
+
return echo.intentGrowthTitle;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function segmentLead(segment: EchoSegment, p: ReturnType<typeof useLocale>['t']['echoPages']): string {
|
|
33
|
+
switch (segment) {
|
|
34
|
+
case 'about-you':
|
|
35
|
+
return p.aboutYouLead;
|
|
36
|
+
case 'continued':
|
|
37
|
+
return p.continuedLead;
|
|
38
|
+
case 'daily':
|
|
39
|
+
return p.dailyLead;
|
|
40
|
+
case 'past-you':
|
|
41
|
+
return p.pastYouLead;
|
|
42
|
+
case 'growth':
|
|
43
|
+
return p.growthLead;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fieldLabelClass =
|
|
48
|
+
'block font-sans text-2xs font-semibold uppercase tracking-wide text-muted-foreground';
|
|
49
|
+
const inputClass =
|
|
50
|
+
'mt-2 w-full min-h-[5rem] resize-y rounded-lg border border-border bg-background px-3 py-2.5 font-sans text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
|
51
|
+
const cardSectionClass =
|
|
52
|
+
'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-md sm:p-6';
|
|
53
|
+
|
|
54
|
+
export default function EchoSegmentPageClient({ segment }: { segment: EchoSegment }) {
|
|
55
|
+
const { t, locale } = useLocale();
|
|
56
|
+
const p = t.echoPages;
|
|
57
|
+
const echo = t.panels.echo;
|
|
58
|
+
const title = segmentTitle(segment, echo);
|
|
59
|
+
const lead = segmentLead(segment, p);
|
|
60
|
+
const factsHeadingId = useId();
|
|
61
|
+
const pageTitleId = 'echo-page-title';
|
|
62
|
+
|
|
63
|
+
const [dailyLine, setDailyLine] = useState('');
|
|
64
|
+
const [growthIntent, setGrowthIntent] = useState('');
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
try {
|
|
68
|
+
const d = localStorage.getItem(STORAGE_DAILY);
|
|
69
|
+
if (d) setDailyLine(d);
|
|
70
|
+
const g = localStorage.getItem(STORAGE_GROWTH);
|
|
71
|
+
if (g) setGrowthIntent(g);
|
|
72
|
+
} catch {
|
|
73
|
+
/* ignore */
|
|
74
|
+
}
|
|
75
|
+
}, []);
|
|
76
|
+
|
|
77
|
+
const persistDaily = useCallback(() => {
|
|
78
|
+
try {
|
|
79
|
+
localStorage.setItem(STORAGE_DAILY, dailyLine);
|
|
80
|
+
} catch {
|
|
81
|
+
/* ignore */
|
|
82
|
+
}
|
|
83
|
+
}, [dailyLine]);
|
|
84
|
+
|
|
85
|
+
const persistGrowth = useCallback(() => {
|
|
86
|
+
try {
|
|
87
|
+
localStorage.setItem(STORAGE_GROWTH, growthIntent);
|
|
88
|
+
} catch {
|
|
89
|
+
/* ignore */
|
|
90
|
+
}
|
|
91
|
+
}, [growthIntent]);
|
|
92
|
+
|
|
93
|
+
const openDailyAsk = useCallback(() => {
|
|
94
|
+
persistDaily();
|
|
95
|
+
openAskModal(p.dailyAskPrefill(dailyLine), 'user');
|
|
96
|
+
}, [dailyLine, p, persistDaily]);
|
|
97
|
+
|
|
98
|
+
const openSegmentAsk = useCallback(() => {
|
|
99
|
+
openAskModal(`${p.parent} / ${title}\n\n`, 'user');
|
|
100
|
+
}, [p.parent, title]);
|
|
101
|
+
|
|
102
|
+
const insightUserPrompt = useMemo(
|
|
103
|
+
() =>
|
|
104
|
+
buildEchoInsightUserPrompt({
|
|
105
|
+
locale: locale as Locale,
|
|
106
|
+
segment,
|
|
107
|
+
segmentTitle: title,
|
|
108
|
+
factsHeading: p.factsHeading,
|
|
109
|
+
emptyTitle: p.emptyFactsTitle,
|
|
110
|
+
emptyBody: p.emptyFactsBody,
|
|
111
|
+
continuedDrafts: p.continuedDrafts,
|
|
112
|
+
continuedTodos: p.continuedTodos,
|
|
113
|
+
subEmptyHint: p.subEmptyHint,
|
|
114
|
+
dailyLineLabel: p.dailyLineLabel,
|
|
115
|
+
dailyLine,
|
|
116
|
+
growthIntentLabel: p.growthIntentLabel,
|
|
117
|
+
growthIntent,
|
|
118
|
+
}),
|
|
119
|
+
[
|
|
120
|
+
locale,
|
|
121
|
+
segment,
|
|
122
|
+
title,
|
|
123
|
+
p.factsHeading,
|
|
124
|
+
p.emptyFactsTitle,
|
|
125
|
+
p.emptyFactsBody,
|
|
126
|
+
p.continuedDrafts,
|
|
127
|
+
p.continuedTodos,
|
|
128
|
+
p.subEmptyHint,
|
|
129
|
+
p.dailyLineLabel,
|
|
130
|
+
dailyLine,
|
|
131
|
+
p.growthIntentLabel,
|
|
132
|
+
growthIntent,
|
|
133
|
+
],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const primaryBtnClass =
|
|
137
|
+
'inline-flex items-center rounded-lg bg-primary px-4 py-2.5 font-sans text-sm font-medium text-primary-foreground transition-opacity duration-150 hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
|
138
|
+
const secondaryBtnClass =
|
|
139
|
+
'inline-flex items-center rounded-lg border border-border bg-background px-4 py-2.5 font-sans text-sm font-medium text-foreground transition-colors duration-150 hover:border-[var(--amber)]/35 hover:bg-[var(--amber-dim)]/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<article
|
|
143
|
+
className="mx-auto max-w-3xl px-4 py-6 sm:px-6 md:py-11"
|
|
144
|
+
aria-labelledby={pageTitleId}
|
|
145
|
+
>
|
|
146
|
+
<EchoHero
|
|
147
|
+
breadcrumbNav={p.breadcrumbNav}
|
|
148
|
+
parentHref="/echo/about-you"
|
|
149
|
+
parent={p.parent}
|
|
150
|
+
heroKicker={p.heroKicker}
|
|
151
|
+
pageTitle={title}
|
|
152
|
+
lead={lead}
|
|
153
|
+
titleId={pageTitleId}
|
|
154
|
+
/>
|
|
155
|
+
|
|
156
|
+
<EchoSegmentNav activeSegment={segment} />
|
|
157
|
+
|
|
158
|
+
<div className="mt-6 space-y-6 sm:mt-8">
|
|
159
|
+
<EchoFactSnapshot
|
|
160
|
+
headingId={factsHeadingId}
|
|
161
|
+
heading={p.factsHeading}
|
|
162
|
+
snapshotBadge={p.snapshotBadge}
|
|
163
|
+
emptyTitle={p.emptyFactsTitle}
|
|
164
|
+
emptyBody={p.emptyFactsBody}
|
|
165
|
+
actions={
|
|
166
|
+
segment === 'about-you' ? (
|
|
167
|
+
<button type="button" onClick={openSegmentAsk} className={secondaryBtnClass}>
|
|
168
|
+
{p.continueAgent}
|
|
169
|
+
</button>
|
|
170
|
+
) : undefined
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
{segment === 'continued' ? (
|
|
174
|
+
<EchoContinuedGroups
|
|
175
|
+
draftsLabel={p.continuedDrafts}
|
|
176
|
+
todosLabel={p.continuedTodos}
|
|
177
|
+
subEmptyHint={p.subEmptyHint}
|
|
178
|
+
footer={
|
|
179
|
+
<button type="button" onClick={openSegmentAsk} className={secondaryBtnClass}>
|
|
180
|
+
{p.continueAgent}
|
|
181
|
+
</button>
|
|
182
|
+
}
|
|
183
|
+
/>
|
|
184
|
+
) : null}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{segment === 'daily' ? (
|
|
188
|
+
<section className={`${cardSectionClass} mt-6`}>
|
|
189
|
+
<label htmlFor="echo-daily-line" className={fieldLabelClass}>
|
|
190
|
+
{p.dailyLineLabel}
|
|
191
|
+
</label>
|
|
192
|
+
<textarea
|
|
193
|
+
id="echo-daily-line"
|
|
194
|
+
value={dailyLine}
|
|
195
|
+
onChange={(e) => setDailyLine(e.target.value)}
|
|
196
|
+
onBlur={persistDaily}
|
|
197
|
+
rows={3}
|
|
198
|
+
placeholder={p.dailyLinePlaceholder}
|
|
199
|
+
className={inputClass}
|
|
200
|
+
/>
|
|
201
|
+
<div className="mt-4">
|
|
202
|
+
<button type="button" onClick={openDailyAsk} className={primaryBtnClass}>
|
|
203
|
+
{p.continueAgent}
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
</section>
|
|
207
|
+
) : null}
|
|
208
|
+
|
|
209
|
+
{segment === 'growth' ? (
|
|
210
|
+
<section className={`${cardSectionClass} mt-6`}>
|
|
211
|
+
<label htmlFor="echo-growth-intent" className={fieldLabelClass}>
|
|
212
|
+
{p.growthIntentLabel}
|
|
213
|
+
</label>
|
|
214
|
+
<textarea
|
|
215
|
+
id="echo-growth-intent"
|
|
216
|
+
value={growthIntent}
|
|
217
|
+
onChange={(e) => setGrowthIntent(e.target.value)}
|
|
218
|
+
onBlur={persistGrowth}
|
|
219
|
+
rows={4}
|
|
220
|
+
placeholder={p.growthIntentPlaceholder}
|
|
221
|
+
className={`${inputClass} min-h-[6.5rem]`}
|
|
222
|
+
/>
|
|
223
|
+
<p className="mt-3 font-sans text-2xs text-muted-foreground">{p.growthSavedNote}</p>
|
|
224
|
+
<div className="mt-4 border-t border-border/60 pt-4">
|
|
225
|
+
<button type="button" onClick={openSegmentAsk} className={secondaryBtnClass}>
|
|
226
|
+
{p.continueAgent}
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
</section>
|
|
230
|
+
) : null}
|
|
231
|
+
|
|
232
|
+
{segment === 'past-you' ? (
|
|
233
|
+
<section className={`${cardSectionClass} mt-6`}>
|
|
234
|
+
<button
|
|
235
|
+
type="button"
|
|
236
|
+
disabled
|
|
237
|
+
title={p.pastYouDisabledHint}
|
|
238
|
+
className="inline-flex cursor-not-allowed items-center rounded-lg border border-dashed border-border bg-muted/20 px-4 py-2.5 font-sans text-sm text-muted-foreground opacity-85"
|
|
239
|
+
>
|
|
240
|
+
{p.pastYouAnother}
|
|
241
|
+
</button>
|
|
242
|
+
<p className="mt-3 font-sans text-2xs text-muted-foreground">{p.pastYouDisabledHint}</p>
|
|
243
|
+
<div className="mt-4 border-t border-border/60 pt-4">
|
|
244
|
+
<button type="button" onClick={openSegmentAsk} className={secondaryBtnClass}>
|
|
245
|
+
{p.continueAgent}
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
248
|
+
</section>
|
|
249
|
+
) : null}
|
|
250
|
+
|
|
251
|
+
<EchoInsightCollapsible
|
|
252
|
+
title={p.insightTitle}
|
|
253
|
+
showLabel={p.insightShow}
|
|
254
|
+
hideLabel={p.insightHide}
|
|
255
|
+
hint={p.insightHint}
|
|
256
|
+
generateLabel={p.generateInsight}
|
|
257
|
+
noAiHint={p.generateInsightNoAi}
|
|
258
|
+
generatingLabel={p.insightGenerating}
|
|
259
|
+
errorPrefix={p.insightErrorPrefix}
|
|
260
|
+
retryLabel={p.insightRetry}
|
|
261
|
+
userPrompt={insightUserPrompt}
|
|
262
|
+
/>
|
|
263
|
+
</article>
|
|
264
|
+
);
|
|
265
|
+
}
|