@geminilight/mindos 0.5.43 → 0.5.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/app/api/ask-sessions/route.ts +6 -4
- package/app/app/api/update-status/route.ts +19 -0
- package/app/components/CreateSpaceModal.tsx +87 -13
- package/app/components/DirPicker.tsx +2 -1
- package/app/components/GuideCard.tsx +38 -26
- package/app/components/HomeContent.tsx +6 -13
- package/app/components/ask/AskContent.tsx +10 -3
- package/app/components/ask/SessionHistory.tsx +39 -3
- package/app/components/explore/ExploreContent.tsx +50 -19
- package/app/components/explore/use-cases.ts +41 -13
- package/app/components/panels/DiscoverPanel.tsx +89 -99
- package/app/components/settings/SettingsContent.tsx +1 -1
- package/app/components/settings/UpdateTab.tsx +145 -28
- package/app/hooks/useAskSession.ts +24 -0
- package/app/lib/i18n-en.ts +25 -4
- package/app/lib/i18n-zh.ts +25 -4
- package/app/lib/utils.ts +11 -0
- package/bin/cli.js +87 -15
- package/bin/lib/startup.js +4 -0
- package/bin/lib/update-status.js +115 -0
- package/package.json +1 -1
- package/scripts/fix-postcss-deps.cjs +4 -2
|
@@ -1,30 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
/** Capability axis — maps to product pillars */
|
|
2
|
+
export type UseCaseCategory = 'knowledge-management' | 'memory-sync' | 'auto-execute' | 'experience-evolution' | 'human-insights' | 'audit-control';
|
|
3
|
+
|
|
4
|
+
/** Scenario axis — maps to user journey phase */
|
|
5
|
+
export type UseCaseScenario = 'first-day' | 'daily' | 'project' | 'advanced';
|
|
2
6
|
|
|
3
7
|
export interface UseCase {
|
|
4
8
|
id: string;
|
|
5
9
|
icon: string;
|
|
6
10
|
category: UseCaseCategory;
|
|
11
|
+
scenario: UseCaseScenario;
|
|
7
12
|
}
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
15
|
* C1-C9 use case definitions.
|
|
11
16
|
* All display text (title, description, prompt) comes from i18n — this file is structure only.
|
|
17
|
+
*
|
|
18
|
+
* Category (capability axis):
|
|
19
|
+
* knowledge-management — Inject, organize, and maintain knowledge
|
|
20
|
+
* memory-sync — Record once, all Agents know
|
|
21
|
+
* auto-execute — One sentence, auto-execute
|
|
22
|
+
* experience-evolution — Gets smarter with use
|
|
23
|
+
* human-insights — Understand and manage relationships
|
|
24
|
+
* audit-control — You have final say
|
|
25
|
+
*
|
|
26
|
+
* Scenario (journey axis):
|
|
27
|
+
* first-day — Onboarding / first-time tasks
|
|
28
|
+
* daily — Everyday workflows
|
|
29
|
+
* project — Project-scoped work
|
|
30
|
+
* advanced — Power-user patterns
|
|
12
31
|
*/
|
|
13
32
|
export const useCases: UseCase[] = [
|
|
14
|
-
{ id: 'c1', icon: '👤', category: '
|
|
15
|
-
{ id: 'c2', icon: '📥', category: '
|
|
16
|
-
{ id: 'c3', icon: '🔄', category: '
|
|
17
|
-
{ id: 'c4', icon: '🔁', category: '
|
|
18
|
-
{ id: 'c5', icon: '💡', category: '
|
|
19
|
-
{ id: 'c6', icon: '🚀', category: '
|
|
20
|
-
{ id: 'c7', icon: '🔍', category: 'knowledge-
|
|
21
|
-
{ id: 'c8', icon: '🤝', category: '
|
|
22
|
-
{ id: 'c9', icon: '🛡️', category: 'advanced' },
|
|
33
|
+
{ id: 'c1', icon: '👤', category: 'memory-sync', scenario: 'first-day' },
|
|
34
|
+
{ id: 'c2', icon: '📥', category: 'knowledge-management', scenario: 'daily' },
|
|
35
|
+
{ id: 'c3', icon: '🔄', category: 'memory-sync', scenario: 'project' },
|
|
36
|
+
{ id: 'c4', icon: '🔁', category: 'experience-evolution', scenario: 'daily' },
|
|
37
|
+
{ id: 'c5', icon: '💡', category: 'auto-execute', scenario: 'daily' },
|
|
38
|
+
{ id: 'c6', icon: '🚀', category: 'auto-execute', scenario: 'project' },
|
|
39
|
+
{ id: 'c7', icon: '🔍', category: 'knowledge-management', scenario: 'project' },
|
|
40
|
+
{ id: 'c8', icon: '🤝', category: 'human-insights', scenario: 'daily' },
|
|
41
|
+
{ id: 'c9', icon: '🛡️', category: 'audit-control', scenario: 'advanced' },
|
|
23
42
|
];
|
|
24
43
|
|
|
25
44
|
export const categories: UseCaseCategory[] = [
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
45
|
+
'knowledge-management',
|
|
46
|
+
'memory-sync',
|
|
47
|
+
'auto-execute',
|
|
48
|
+
'experience-evolution',
|
|
49
|
+
'human-insights',
|
|
50
|
+
'audit-control',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export const scenarios: UseCaseScenario[] = [
|
|
54
|
+
'first-day',
|
|
55
|
+
'daily',
|
|
56
|
+
'project',
|
|
29
57
|
'advanced',
|
|
30
58
|
];
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react';
|
|
4
3
|
import Link from 'next/link';
|
|
5
|
-
import {
|
|
4
|
+
import { Lightbulb, Blocks, Zap, LayoutTemplate, ChevronRight, User, Download, RefreshCw, Repeat, Rocket, Search, Handshake, ShieldCheck } from 'lucide-react';
|
|
6
5
|
import PanelHeader from './PanelHeader';
|
|
7
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
8
7
|
import { useCases } from '@/components/explore/use-cases';
|
|
@@ -14,56 +13,61 @@ interface DiscoverPanelProps {
|
|
|
14
13
|
onMaximize?: () => void;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
|
-
/**
|
|
18
|
-
function
|
|
16
|
+
/** Navigation entry — clickable row linking to a page or showing coming soon */
|
|
17
|
+
function NavEntry({
|
|
19
18
|
icon,
|
|
20
19
|
title,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
badge,
|
|
21
|
+
href,
|
|
22
|
+
onClick,
|
|
24
23
|
}: {
|
|
25
24
|
icon: React.ReactNode;
|
|
26
25
|
title: string;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
badge?: React.ReactNode;
|
|
27
|
+
href?: string;
|
|
28
|
+
onClick?: () => void;
|
|
30
29
|
}) {
|
|
31
|
-
const
|
|
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 }) {
|
|
32
49
|
return (
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
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>
|
|
50
|
+
<span className="text-2xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground shrink-0">
|
|
51
|
+
{label}
|
|
52
|
+
</span>
|
|
49
53
|
);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
/** Compact use case row
|
|
56
|
+
/** Compact use case row */
|
|
53
57
|
function UseCaseRow({
|
|
54
58
|
icon,
|
|
55
59
|
title,
|
|
56
60
|
prompt,
|
|
57
61
|
tryLabel,
|
|
58
62
|
}: {
|
|
59
|
-
icon:
|
|
63
|
+
icon: React.ReactNode;
|
|
60
64
|
title: string;
|
|
61
65
|
prompt: string;
|
|
62
66
|
tryLabel: string;
|
|
63
67
|
}) {
|
|
64
68
|
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-
|
|
69
|
+
<div className="group flex items-center gap-2.5 px-4 py-1.5 hover:bg-muted/50 transition-colors rounded-sm mx-1">
|
|
70
|
+
<span className="text-muted-foreground shrink-0">{icon}</span>
|
|
67
71
|
<span className="text-xs text-foreground truncate flex-1">{title}</span>
|
|
68
72
|
<button
|
|
69
73
|
onClick={() => openAskModal(prompt, 'user')}
|
|
@@ -75,29 +79,18 @@ function UseCaseRow({
|
|
|
75
79
|
);
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
}
|
|
82
|
+
/** Map use case id → lucide icon */
|
|
83
|
+
const useCaseIcons: Record<string, React.ReactNode> = {
|
|
84
|
+
c1: <User size={12} />, // Inject Identity
|
|
85
|
+
c2: <Download size={12} />, // Save Information
|
|
86
|
+
c3: <RefreshCw size={12} />, // Cross-Agent Handoff
|
|
87
|
+
c4: <Repeat size={12} />, // Experience → SOP
|
|
88
|
+
c5: <Lightbulb size={12} />, // Capture Ideas
|
|
89
|
+
c6: <Rocket size={12} />, // Project Cold Start
|
|
90
|
+
c7: <Search size={12} />, // Research & Archive
|
|
91
|
+
c8: <Handshake size={12} />, // Network Management
|
|
92
|
+
c9: <ShieldCheck size={12} />, // Audit & Correct
|
|
93
|
+
};
|
|
101
94
|
|
|
102
95
|
export default function DiscoverPanel({ active, maximized, onMaximize }: DiscoverPanelProps) {
|
|
103
96
|
const { t } = useLocale();
|
|
@@ -117,54 +110,51 @@ export default function DiscoverPanel({ active, maximized, onMaximize }: Discove
|
|
|
117
110
|
<div className={`flex flex-col h-full ${active ? '' : 'hidden'}`}>
|
|
118
111
|
<PanelHeader title={d.title} maximized={maximized} onMaximize={onMaximize} />
|
|
119
112
|
<div className="flex-1 overflow-y-auto min-h-0">
|
|
120
|
-
{/*
|
|
121
|
-
<
|
|
122
|
-
<
|
|
123
|
-
{
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
title={d.pluginMarket}
|
|
145
|
-
description={d.pluginMarketDesc}
|
|
146
|
-
comingSoonLabel={d.comingSoon}
|
|
147
|
-
/>
|
|
113
|
+
{/* Navigation entries */}
|
|
114
|
+
<div className="py-2">
|
|
115
|
+
<NavEntry
|
|
116
|
+
icon={<Lightbulb size={14} className="text-[var(--amber)]" />}
|
|
117
|
+
title={d.useCases}
|
|
118
|
+
badge={<span className="text-2xs tabular-nums text-muted-foreground">{useCases.length}</span>}
|
|
119
|
+
href="/explore"
|
|
120
|
+
/>
|
|
121
|
+
<NavEntry
|
|
122
|
+
icon={<Blocks size={14} className="text-muted-foreground" />}
|
|
123
|
+
title={d.pluginMarket}
|
|
124
|
+
badge={<ComingSoonBadge label={d.comingSoon} />}
|
|
125
|
+
/>
|
|
126
|
+
<NavEntry
|
|
127
|
+
icon={<Zap size={14} className="text-muted-foreground" />}
|
|
128
|
+
title={d.skillMarket}
|
|
129
|
+
badge={<ComingSoonBadge label={d.comingSoon} />}
|
|
130
|
+
/>
|
|
131
|
+
<NavEntry
|
|
132
|
+
icon={<LayoutTemplate size={14} className="text-muted-foreground" />}
|
|
133
|
+
title={d.spaceTemplates}
|
|
134
|
+
badge={<ComingSoonBadge label={d.comingSoon} />}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
148
137
|
|
|
149
138
|
<div className="mx-4 border-t border-border" />
|
|
150
139
|
|
|
151
|
-
{/*
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
140
|
+
{/* Quick try — use case list */}
|
|
141
|
+
<div className="py-2">
|
|
142
|
+
<div className="px-4 py-1.5">
|
|
143
|
+
<span className="text-2xs font-medium text-muted-foreground uppercase tracking-wider">{d.useCases}</span>
|
|
144
|
+
</div>
|
|
145
|
+
{useCases.map(uc => {
|
|
146
|
+
const data = getUseCaseText(uc.id);
|
|
147
|
+
if (!data) return null;
|
|
148
|
+
return (
|
|
149
|
+
<UseCaseRow
|
|
150
|
+
key={uc.id}
|
|
151
|
+
icon={useCaseIcons[uc.id] || <Lightbulb size={12} />}
|
|
152
|
+
title={data.title}
|
|
153
|
+
prompt={data.prompt}
|
|
154
|
+
tryLabel={d.tryIt}
|
|
155
|
+
/>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
168
158
|
</div>
|
|
169
159
|
</div>
|
|
170
160
|
</div>
|
|
@@ -153,8 +153,8 @@ export default function SettingsContent({ visible, initialTab, variant, onClose
|
|
|
153
153
|
{ id: 'ai', label: t.settings.tabs.ai, icon: <Sparkles size={iconSize} /> },
|
|
154
154
|
{ id: 'mcp', label: t.settings.tabs.mcp ?? 'MCP & Skills', icon: <Plug size={iconSize} /> },
|
|
155
155
|
{ id: 'knowledge', label: t.settings.tabs.knowledge, icon: <Settings size={iconSize} /> },
|
|
156
|
-
{ id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
|
|
157
156
|
{ id: 'appearance', label: t.settings.tabs.appearance, icon: <Palette size={iconSize} /> },
|
|
157
|
+
{ id: 'sync', label: t.settings.tabs.sync ?? 'Sync', icon: <RefreshCw size={iconSize} /> },
|
|
158
158
|
{ id: 'update', label: t.settings.tabs.update ?? 'Update', icon: <Download size={iconSize} />, badge: hasUpdate },
|
|
159
159
|
];
|
|
160
160
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
|
-
import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, ExternalLink } from 'lucide-react';
|
|
4
|
+
import { Download, RefreshCw, CheckCircle2, AlertCircle, Loader2, ExternalLink, Circle } from 'lucide-react';
|
|
5
5
|
import { apiFetch } from '@/lib/api';
|
|
6
6
|
import { useLocale } from '@/lib/LocaleContext';
|
|
7
7
|
|
|
@@ -11,18 +11,53 @@ interface UpdateInfo {
|
|
|
11
11
|
hasUpdate: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
interface StageInfo {
|
|
15
|
+
id: string;
|
|
16
|
+
status: 'pending' | 'running' | 'done' | 'failed';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UpdateStatus {
|
|
20
|
+
stage: string;
|
|
21
|
+
stages: StageInfo[];
|
|
22
|
+
error: string | null;
|
|
23
|
+
version: { from: string | null; to: string | null } | null;
|
|
24
|
+
startedAt: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
type UpdateState = 'idle' | 'checking' | 'updating' | 'updated' | 'error' | 'timeout';
|
|
15
28
|
|
|
16
29
|
const CHANGELOG_URL = 'https://github.com/GeminiLight/MindOS/releases';
|
|
17
|
-
const POLL_INTERVAL =
|
|
18
|
-
const POLL_TIMEOUT =
|
|
30
|
+
const POLL_INTERVAL = 3_000;
|
|
31
|
+
const POLL_TIMEOUT = 5 * 60 * 1000; // 5 minutes
|
|
32
|
+
|
|
33
|
+
const STAGE_LABELS: Record<string, { en: string; zh: string }> = {
|
|
34
|
+
downloading: { en: 'Downloading update', zh: '下载更新' },
|
|
35
|
+
skills: { en: 'Updating skills', zh: '更新 Skills' },
|
|
36
|
+
rebuilding: { en: 'Rebuilding app', zh: '重新构建应用' },
|
|
37
|
+
restarting: { en: 'Restarting server', zh: '重启服务' },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function StageIcon({ status }: { status: string }) {
|
|
41
|
+
switch (status) {
|
|
42
|
+
case 'done':
|
|
43
|
+
return <CheckCircle2 size={14} className="text-success shrink-0" />;
|
|
44
|
+
case 'running':
|
|
45
|
+
return <Loader2 size={14} className="animate-spin shrink-0" style={{ color: 'var(--amber)' }} />;
|
|
46
|
+
case 'failed':
|
|
47
|
+
return <AlertCircle size={14} className="text-destructive shrink-0" />;
|
|
48
|
+
default:
|
|
49
|
+
return <Circle size={14} className="text-muted-foreground/40 shrink-0" />;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
19
52
|
|
|
20
53
|
export function UpdateTab() {
|
|
21
|
-
const { t } = useLocale();
|
|
54
|
+
const { t, locale } = useLocale();
|
|
22
55
|
const u = t.settings.update;
|
|
23
56
|
const [info, setInfo] = useState<UpdateInfo | null>(null);
|
|
24
57
|
const [state, setState] = useState<UpdateState>('idle');
|
|
25
58
|
const [errorMsg, setErrorMsg] = useState('');
|
|
59
|
+
const [stages, setStages] = useState<StageInfo[]>([]);
|
|
60
|
+
const [updateError, setUpdateError] = useState<string | null>(null);
|
|
26
61
|
const pollRef = useRef<ReturnType<typeof setInterval>>(undefined);
|
|
27
62
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
28
63
|
const originalVersion = useRef<string>('');
|
|
@@ -43,16 +78,23 @@ export function UpdateTab() {
|
|
|
43
78
|
|
|
44
79
|
useEffect(() => { checkUpdate(); }, [checkUpdate]);
|
|
45
80
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
clearTimeout(timeoutRef.current);
|
|
50
|
-
};
|
|
81
|
+
const cleanup = useCallback(() => {
|
|
82
|
+
clearInterval(pollRef.current);
|
|
83
|
+
clearTimeout(timeoutRef.current);
|
|
51
84
|
}, []);
|
|
52
85
|
|
|
86
|
+
useEffect(() => cleanup, [cleanup]);
|
|
87
|
+
|
|
53
88
|
const handleUpdate = useCallback(async () => {
|
|
54
89
|
setState('updating');
|
|
55
90
|
setErrorMsg('');
|
|
91
|
+
setUpdateError(null);
|
|
92
|
+
setStages([
|
|
93
|
+
{ id: 'downloading', status: 'pending' },
|
|
94
|
+
{ id: 'skills', status: 'pending' },
|
|
95
|
+
{ id: 'rebuilding', status: 'pending' },
|
|
96
|
+
{ id: 'restarting', status: 'pending' },
|
|
97
|
+
]);
|
|
56
98
|
|
|
57
99
|
try {
|
|
58
100
|
await apiFetch('/api/update', { method: 'POST' });
|
|
@@ -60,26 +102,67 @@ export function UpdateTab() {
|
|
|
60
102
|
// Expected — server may die during update
|
|
61
103
|
}
|
|
62
104
|
|
|
105
|
+
// Poll update-status for stage progress
|
|
63
106
|
pollRef.current = setInterval(async () => {
|
|
107
|
+
// Try status endpoint first (may fail when server is restarting)
|
|
64
108
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
109
|
+
const status = await apiFetch<UpdateStatus>('/api/update-status', { timeout: 5000 });
|
|
110
|
+
|
|
111
|
+
if (status.stages?.length > 0) {
|
|
112
|
+
setStages(status.stages);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (status.stage === 'failed') {
|
|
116
|
+
cleanup();
|
|
117
|
+
setUpdateError(status.error || 'Update failed');
|
|
118
|
+
setState('error');
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (status.stage === 'done') {
|
|
123
|
+
// Verify version actually changed
|
|
124
|
+
try {
|
|
125
|
+
const data = await apiFetch<UpdateInfo>('/api/update-check');
|
|
126
|
+
if (data.current !== originalVersion.current) {
|
|
127
|
+
cleanup();
|
|
128
|
+
setInfo(data);
|
|
129
|
+
setState('updated');
|
|
130
|
+
setTimeout(() => window.location.reload(), 2000);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
} catch { /* new server may not be fully ready */ }
|
|
72
134
|
}
|
|
73
135
|
} catch {
|
|
74
|
-
// Server
|
|
136
|
+
// Server restarting — also try update-check as fallback
|
|
137
|
+
try {
|
|
138
|
+
const data = await apiFetch<UpdateInfo>('/api/update-check', { timeout: 5000 });
|
|
139
|
+
if (data.current !== originalVersion.current) {
|
|
140
|
+
cleanup();
|
|
141
|
+
setStages(prev => prev.map(s => ({ ...s, status: 'done' as const })));
|
|
142
|
+
setInfo(data);
|
|
143
|
+
setState('updated');
|
|
144
|
+
setTimeout(() => window.location.reload(), 2000);
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
// Both endpoints down — server still restarting
|
|
148
|
+
}
|
|
75
149
|
}
|
|
76
150
|
}, POLL_INTERVAL);
|
|
77
151
|
|
|
78
152
|
timeoutRef.current = setTimeout(() => {
|
|
79
|
-
|
|
153
|
+
cleanup();
|
|
80
154
|
setState('timeout');
|
|
81
155
|
}, POLL_TIMEOUT);
|
|
82
|
-
}, []);
|
|
156
|
+
}, [cleanup]);
|
|
157
|
+
|
|
158
|
+
const handleRetry = useCallback(() => {
|
|
159
|
+
setUpdateError(null);
|
|
160
|
+
handleUpdate();
|
|
161
|
+
}, [handleUpdate]);
|
|
162
|
+
|
|
163
|
+
const lang = locale === 'zh' ? 'zh' : 'en';
|
|
164
|
+
const doneCount = stages.filter(s => s.status === 'done').length;
|
|
165
|
+
const progress = stages.length > 0 ? Math.round((doneCount / stages.length) * 100) : 0;
|
|
83
166
|
|
|
84
167
|
return (
|
|
85
168
|
<div className="space-y-5">
|
|
@@ -114,11 +197,27 @@ export function UpdateTab() {
|
|
|
114
197
|
)}
|
|
115
198
|
|
|
116
199
|
{state === 'updating' && (
|
|
117
|
-
<div className="space-y-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{
|
|
200
|
+
<div className="space-y-3">
|
|
201
|
+
{/* Stage list */}
|
|
202
|
+
<div className="space-y-1.5">
|
|
203
|
+
{stages.map((s) => (
|
|
204
|
+
<div key={s.id} className="flex items-center gap-2 text-xs">
|
|
205
|
+
<StageIcon status={s.status} />
|
|
206
|
+
<span className={s.status === 'pending' ? 'text-muted-foreground/50' : s.status === 'running' ? 'text-foreground' : 'text-muted-foreground'}>
|
|
207
|
+
{STAGE_LABELS[s.id]?.[lang] ?? s.id}
|
|
208
|
+
</span>
|
|
209
|
+
</div>
|
|
210
|
+
))}
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
{/* Progress bar */}
|
|
214
|
+
<div className="h-1 rounded-full bg-muted overflow-hidden">
|
|
215
|
+
<div
|
|
216
|
+
className="h-full rounded-full transition-all duration-500 ease-out"
|
|
217
|
+
style={{ width: `${Math.max(progress, 5)}%`, background: 'var(--amber)' }}
|
|
218
|
+
/>
|
|
121
219
|
</div>
|
|
220
|
+
|
|
122
221
|
<p className="text-2xs text-muted-foreground">
|
|
123
222
|
{u?.updatingHint ?? 'This may take 1–3 minutes. Do not close this page.'}
|
|
124
223
|
</p>
|
|
@@ -133,21 +232,39 @@ export function UpdateTab() {
|
|
|
133
232
|
)}
|
|
134
233
|
|
|
135
234
|
{state === 'timeout' && (
|
|
136
|
-
<div className="space-y-
|
|
235
|
+
<div className="space-y-2">
|
|
137
236
|
<div className="flex items-center gap-2 text-xs text-amber-600 dark:text-amber-400">
|
|
138
237
|
<AlertCircle size={13} />
|
|
139
238
|
{u?.timeout ?? 'Update may still be in progress.'}
|
|
140
239
|
</div>
|
|
141
240
|
<p className="text-2xs text-muted-foreground">
|
|
142
|
-
{u?.timeoutHint ?? '
|
|
241
|
+
{u?.timeoutHint ?? 'The server may need more time to rebuild. Try refreshing.'}
|
|
143
242
|
</p>
|
|
243
|
+
<button
|
|
244
|
+
onClick={() => window.location.reload()}
|
|
245
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
246
|
+
>
|
|
247
|
+
<RefreshCw size={12} />
|
|
248
|
+
{u?.refreshButton ?? 'Refresh Page'}
|
|
249
|
+
</button>
|
|
144
250
|
</div>
|
|
145
251
|
)}
|
|
146
252
|
|
|
147
253
|
{state === 'error' && (
|
|
148
|
-
<div className="
|
|
149
|
-
<
|
|
150
|
-
|
|
254
|
+
<div className="space-y-2">
|
|
255
|
+
<div className="flex items-center gap-2 text-xs text-destructive">
|
|
256
|
+
<AlertCircle size={13} />
|
|
257
|
+
{updateError || errorMsg}
|
|
258
|
+
</div>
|
|
259
|
+
{updateError && (
|
|
260
|
+
<button
|
|
261
|
+
onClick={handleRetry}
|
|
262
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
|
263
|
+
>
|
|
264
|
+
<RefreshCw size={12} />
|
|
265
|
+
{u?.retryButton ?? 'Retry Update'}
|
|
266
|
+
</button>
|
|
267
|
+
)}
|
|
151
268
|
</div>
|
|
152
269
|
)}
|
|
153
270
|
</div>
|
|
@@ -60,6 +60,18 @@ async function removeSession(id: string): Promise<void> {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
async function removeSessions(ids: string[]): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
await fetch('/api/ask-sessions', {
|
|
66
|
+
method: 'DELETE',
|
|
67
|
+
headers: { 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify({ ids }),
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
// ignore persistence errors
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
63
75
|
export function useAskSession(currentFile?: string) {
|
|
64
76
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
65
77
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
|
@@ -166,6 +178,17 @@ export function useAskSession(currentFile?: string) {
|
|
|
166
178
|
[activeSessionId, currentFile, sessions],
|
|
167
179
|
);
|
|
168
180
|
|
|
181
|
+
const clearAllSessions = useCallback(() => {
|
|
182
|
+
const allIds = sessions.map(s => s.id);
|
|
183
|
+
void removeSessions(allIds);
|
|
184
|
+
|
|
185
|
+
const fresh = createSession(currentFile);
|
|
186
|
+
setActiveSessionId(fresh.id);
|
|
187
|
+
setMessages([]);
|
|
188
|
+
setSessions([fresh]);
|
|
189
|
+
void upsertSession(fresh);
|
|
190
|
+
}, [currentFile, sessions]);
|
|
191
|
+
|
|
169
192
|
return {
|
|
170
193
|
messages,
|
|
171
194
|
setMessages,
|
|
@@ -177,5 +200,6 @@ export function useAskSession(currentFile?: string) {
|
|
|
177
200
|
resetSession,
|
|
178
201
|
loadSession,
|
|
179
202
|
deleteSession,
|
|
203
|
+
clearAllSessions,
|
|
180
204
|
};
|
|
181
205
|
}
|