principles-disciple 1.12.0 → 1.13.0
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/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/scripts/sync-plugin.mjs +156 -1
- package/src/commands/nocturnal-train.ts +11 -12
- package/src/core/evolution-reducer.ts +31 -4
- package/src/core/nocturnal-trinity.ts +19 -4
- package/src/core/principle-tree-ledger.ts +27 -7
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/thinking-os-parser.ts +36 -44
- package/src/index.ts +7 -3
- package/src/service/nocturnal-service.ts +11 -7
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +18 -3
- package/templates/langs/en/principles/THINKING_OS.md +13 -0
- package/templates/langs/zh/principles/THINKING_OS.md +13 -0
- package/ui/src/i18n/ui.ts +34 -9
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import { ChevronLeft } from 'lucide-react';
|
|
1
|
+
import React, { useEffect, useState, useMemo } from 'react';
|
|
2
|
+
import { ChevronLeft, Search, ArrowUpDown, Info } from 'lucide-react';
|
|
3
3
|
import { api } from '../api';
|
|
4
4
|
import type { ThinkingOverviewResponse, ThinkingModelDetailResponse } from '../types';
|
|
5
|
-
import { EmptyState } from '../charts';
|
|
5
|
+
import { EmptyState, LineChart, StatusBadge } from '../charts';
|
|
6
6
|
import { useI18n } from '../i18n/ui';
|
|
7
7
|
import { formatPercent, formatDate } from '../utils/format';
|
|
8
8
|
import { Loading, ErrorState } from '../components';
|
|
9
9
|
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Recommendation badge helper
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
type BadgeVariant = 'success' | 'warning' | 'neutral';
|
|
15
|
+
|
|
16
|
+
const REC_BADGE: Record<string, { variant: BadgeVariant; label: (t: (k: string) => string) => string }> = {
|
|
17
|
+
reinforce: { variant: 'success', label: (t) => t('thinkingModels.reinforce') },
|
|
18
|
+
rework: { variant: 'warning', label: (t) => t('thinkingModels.rework') },
|
|
19
|
+
archive: { variant: 'neutral', label: (t) => t('thinkingModels.archive') },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// ThinkingModelsPage — Redesigned Layout
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
10
26
|
export function ThinkingModelsPage() {
|
|
11
27
|
const { t } = useI18n();
|
|
12
28
|
const [data, setData] = useState<ThinkingOverviewResponse | null>(null);
|
|
@@ -14,6 +30,11 @@ export function ThinkingModelsPage() {
|
|
|
14
30
|
const [selectedModel, setSelectedModel] = useState('');
|
|
15
31
|
const [error, setError] = useState('');
|
|
16
32
|
|
|
33
|
+
// Filters
|
|
34
|
+
const [recFilter, setRecFilter] = useState('all');
|
|
35
|
+
const [search, setSearch] = useState('');
|
|
36
|
+
const [sortBy, setSortBy] = useState<'hits' | 'successRate' | 'name'>('hits');
|
|
37
|
+
|
|
17
38
|
useEffect(() => {
|
|
18
39
|
api.getThinkingOverview().then((value) => {
|
|
19
40
|
setData(value);
|
|
@@ -26,11 +47,37 @@ export function ThinkingModelsPage() {
|
|
|
26
47
|
api.getThinkingModelDetail(selectedModel).then(setDetail).catch((err) => setError(String(err)));
|
|
27
48
|
}, [selectedModel]);
|
|
28
49
|
|
|
50
|
+
// Filtered + sorted model list
|
|
51
|
+
const filteredModels = useMemo(() => {
|
|
52
|
+
if (!data) return [];
|
|
53
|
+
let models = [...data.topModels];
|
|
54
|
+
if (recFilter !== 'all') {
|
|
55
|
+
models = models.filter(m => m.recommendation === recFilter);
|
|
56
|
+
}
|
|
57
|
+
if (search) {
|
|
58
|
+
const q = search.toLowerCase();
|
|
59
|
+
models = models.filter(m =>
|
|
60
|
+
m.name.toLowerCase().includes(q) ||
|
|
61
|
+
(m.commonScenarios ?? []).some(s => s.toLowerCase().includes(q))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
models.sort((a, b) => {
|
|
65
|
+
if (sortBy === 'hits') return b.hits - a.hits;
|
|
66
|
+
if (sortBy === 'successRate') return b.successRate - a.successRate;
|
|
67
|
+
return a.name.localeCompare(b.name);
|
|
68
|
+
});
|
|
69
|
+
return models;
|
|
70
|
+
}, [data, recFilter, search, sortBy]);
|
|
71
|
+
|
|
29
72
|
if (error) return <ErrorState error={error} />;
|
|
30
73
|
if (!data) return <Loading />;
|
|
31
74
|
|
|
75
|
+
const totalHits = data.topModels.reduce((sum, m) => sum + m.hits, 0);
|
|
76
|
+
const hasData = totalHits > 0;
|
|
77
|
+
|
|
32
78
|
return (
|
|
33
79
|
<div className="page">
|
|
80
|
+
{/* ── Header ── */}
|
|
34
81
|
<header className="page-header">
|
|
35
82
|
<div>
|
|
36
83
|
<h2>{t('thinkingModels.pageTitle')}</h2>
|
|
@@ -43,85 +90,256 @@ export function ThinkingModelsPage() {
|
|
|
43
90
|
</div>
|
|
44
91
|
</header>
|
|
45
92
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
93
|
+
{!hasData ? (
|
|
94
|
+
/* ── No data yet: show model definitions grid ── */
|
|
95
|
+
<section className="panel" style={{ marginBottom: 'var(--space-4)' }}>
|
|
96
|
+
<div style={{ textAlign: 'center', padding: 'var(--space-5)', color: 'var(--text-secondary)' }}>
|
|
97
|
+
<div style={{ fontSize: '2rem', marginBottom: 8 }}>🧠</div>
|
|
98
|
+
<h3 style={{ marginBottom: 4 }}>{t('thinkingModels.noDataTitle') || '思维模型定义'}</h3>
|
|
99
|
+
<p style={{ fontSize: '0.85rem', maxWidth: 500, margin: '0 auto 24px' }}>
|
|
100
|
+
{t('thinkingModels.noDataDesc') || '以下是 10 个思维模型的定义。当 AI 开始使用后,这里会显示每个模型的使用统计。'}
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 12 }}>
|
|
104
|
+
{data.topModels.map(model => (
|
|
105
|
+
<div
|
|
106
|
+
key={model.modelId}
|
|
107
|
+
style={{
|
|
108
|
+
padding: 12,
|
|
109
|
+
border: '1px solid var(--border)',
|
|
110
|
+
borderRadius: 8,
|
|
111
|
+
background: 'var(--bg-sunken)',
|
|
112
|
+
cursor: 'pointer',
|
|
113
|
+
}}
|
|
114
|
+
onClick={() => { setSelectedModel(model.modelId); setDetail(null); }}
|
|
115
|
+
>
|
|
116
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 6 }}>
|
|
117
|
+
<strong style={{ fontSize: '0.85rem' }}>{model.modelId}: {model.name}</strong>
|
|
62
118
|
</div>
|
|
63
|
-
|
|
119
|
+
<p style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', margin: '0 0 8px', lineHeight: 1.4 }}>
|
|
120
|
+
{model.description}
|
|
121
|
+
</p>
|
|
122
|
+
{model.commonScenarios.length > 0 && (
|
|
123
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
|
124
|
+
{model.commonScenarios.slice(0, 3).map(s => (
|
|
125
|
+
<span key={s} style={{ fontSize: '0.65rem', padding: '1px 6px', background: 'rgba(91,139,160,0.1)', borderRadius: 3, color: 'var(--info)' }}>
|
|
126
|
+
{s}
|
|
127
|
+
</span>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
64
132
|
))}
|
|
65
133
|
</div>
|
|
66
134
|
</section>
|
|
135
|
+
) : (
|
|
136
|
+
/* ── Has data: full dashboard ── */
|
|
137
|
+
<>
|
|
138
|
+
{/* Coverage Trend */}
|
|
139
|
+
{data.coverageTrend.length >= 1 && (
|
|
140
|
+
<section className="panel" style={{ marginBottom: 'var(--space-4)' }}>
|
|
141
|
+
<h3 style={{ fontSize: '0.85rem', fontWeight: 600, marginBottom: 8 }}>
|
|
142
|
+
{t('thinkingModels.coverageTrend')}
|
|
143
|
+
</h3>
|
|
144
|
+
<LineChart
|
|
145
|
+
data={data.coverageTrend.map(d => ({ label: d.day.slice(5), value: Math.round(d.coverageRate * 100) }))}
|
|
146
|
+
width={560}
|
|
147
|
+
height={140}
|
|
148
|
+
color="var(--accent)"
|
|
149
|
+
showGrid
|
|
150
|
+
showDots
|
|
151
|
+
showArea
|
|
152
|
+
/>
|
|
153
|
+
</section>
|
|
154
|
+
)}
|
|
67
155
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
<
|
|
156
|
+
{/* Search + Sort + Filter */}
|
|
157
|
+
<div style={{ display: 'flex', gap: 8, marginBottom: 'var(--space-3)', alignItems: 'center', flexWrap: 'wrap' }}>
|
|
158
|
+
<div style={{ position: 'relative', flex: '1 1 200px' }}>
|
|
159
|
+
<Search size={14} style={{ position: 'absolute', left: 8, top: '50%', transform: 'translateY(-50%)', color: 'var(--text-secondary)' }} />
|
|
160
|
+
<input
|
|
161
|
+
type="text"
|
|
162
|
+
placeholder={t('common.search') || 'Search...'}
|
|
163
|
+
value={search}
|
|
164
|
+
onChange={e => setSearch(e.target.value)}
|
|
165
|
+
style={{ paddingLeft: 28, width: '100%', padding: '6px 8px 6px 28px', border: '1px solid var(--border)', borderRadius: 6, background: 'var(--bg-panel)', color: 'var(--text-primary)', fontSize: '0.8rem' }}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
<button
|
|
169
|
+
onClick={() => setSortBy(prev => prev === 'hits' ? 'successRate' : prev === 'successRate' ? 'name' : 'hits')}
|
|
170
|
+
style={{ display: 'flex', alignItems: 'center', gap: 4, padding: '6px 10px', border: '1px solid var(--border)', borderRadius: 6, background: 'var(--bg-panel)', color: 'var(--text-secondary)', cursor: 'pointer', fontSize: '0.75rem' }}
|
|
171
|
+
>
|
|
172
|
+
<ArrowUpDown size={14} />
|
|
173
|
+
{sortBy === 'hits' ? 'Hits' : sortBy === 'successRate' ? 'Success' : 'Name'}
|
|
174
|
+
</button>
|
|
175
|
+
<div style={{ display: 'flex', gap: 4 }}>
|
|
176
|
+
{['all', 'reinforce', 'rework', 'archive'].map(key => (
|
|
73
177
|
<button
|
|
74
|
-
|
|
75
|
-
onClick={() =>
|
|
76
|
-
|
|
178
|
+
key={key}
|
|
179
|
+
onClick={() => setRecFilter(key)}
|
|
180
|
+
style={{
|
|
181
|
+
padding: '3px 8px',
|
|
182
|
+
border: `1px solid ${recFilter === key ? 'var(--accent)' : 'var(--border)'}`,
|
|
183
|
+
borderRadius: 4,
|
|
184
|
+
background: recFilter === key ? 'rgba(91, 139, 160, 0.15)' : 'transparent',
|
|
185
|
+
color: recFilter === key ? 'var(--accent)' : 'var(--text-secondary)',
|
|
186
|
+
cursor: 'pointer',
|
|
187
|
+
fontSize: '0.7rem',
|
|
188
|
+
}}
|
|
77
189
|
>
|
|
78
|
-
|
|
190
|
+
{key === 'all' ? 'All' : REC_BADGE[key]?.label(t)}
|
|
79
191
|
</button>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
{/* Two-column layout: Model List + Detail */}
|
|
197
|
+
<div className="grid two-columns wide-right">
|
|
198
|
+
{/* Left: Model List */}
|
|
199
|
+
<section className="panel">
|
|
200
|
+
<div className="list-table">
|
|
201
|
+
{filteredModels.map((item) => (
|
|
202
|
+
<button
|
|
203
|
+
className={`table-row ${selectedModel === item.modelId ? 'active' : ''}`}
|
|
204
|
+
key={item.modelId}
|
|
205
|
+
onClick={() => { setSelectedModel(item.modelId); setDetail(null); }}
|
|
206
|
+
>
|
|
207
|
+
<div>
|
|
208
|
+
<strong>{item.name}</strong>
|
|
209
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: 180, display: 'block' }}>
|
|
210
|
+
{item.commonScenarios.join(', ') || '—'}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
214
|
+
<span style={{ fontWeight: 600, fontSize: '0.85rem' }}>{item.hits}</span>
|
|
215
|
+
{REC_BADGE[item.recommendation] && (
|
|
216
|
+
<StatusBadge variant={REC_BADGE[item.recommendation].variant}>
|
|
217
|
+
{REC_BADGE[item.recommendation].label(t)}
|
|
218
|
+
</StatusBadge>
|
|
219
|
+
)}
|
|
220
|
+
</div>
|
|
221
|
+
</button>
|
|
222
|
+
))}
|
|
223
|
+
{filteredModels.length === 0 && (
|
|
224
|
+
<div style={{ padding: 'var(--space-4)', textAlign: 'center', color: 'var(--text-secondary)', fontSize: '0.85rem' }}>
|
|
225
|
+
No models match your filters.
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
85
228
|
</div>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<strong>{item.scenario}</strong>
|
|
101
|
-
<span>{item.hits}</span>
|
|
229
|
+
</section>
|
|
230
|
+
|
|
231
|
+
{/* Right: Detail Panel */}
|
|
232
|
+
<section className="panel">
|
|
233
|
+
{!detail && <EmptyState title={t('thinkingModels.emptyTitle')} description={t('thinkingModels.emptyDesc')} />}
|
|
234
|
+
{detail && (
|
|
235
|
+
<div className="detail-stack">
|
|
236
|
+
<div className="detail-header">
|
|
237
|
+
<button className="back-button" onClick={() => setDetail(null)} title="Back">
|
|
238
|
+
<ChevronLeft strokeWidth={1.75} size={18} />
|
|
239
|
+
</button>
|
|
240
|
+
<div>
|
|
241
|
+
<h3>{detail.modelMeta.name}</h3>
|
|
242
|
+
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>{detail.modelMeta.description}</p>
|
|
102
243
|
</div>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
244
|
+
{REC_BADGE[detail.modelMeta.recommendation] && (
|
|
245
|
+
<StatusBadge variant={REC_BADGE[detail.modelMeta.recommendation].variant}>
|
|
246
|
+
{REC_BADGE[detail.modelMeta.recommendation].label(t)}
|
|
247
|
+
</StatusBadge>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* Usage Trend */}
|
|
252
|
+
{detail.usageTrend.length >= 1 && (
|
|
253
|
+
<article>
|
|
254
|
+
<h4 style={{ fontSize: '0.8rem', fontWeight: 600, marginBottom: 8 }}>
|
|
255
|
+
{t('thinkingModels.usageTrend') || 'Usage Trend'}
|
|
256
|
+
</h4>
|
|
257
|
+
<LineChart
|
|
258
|
+
data={detail.usageTrend.map(d => ({ label: d.day.slice(5), value: d.hits }))}
|
|
259
|
+
width={500}
|
|
260
|
+
height={100}
|
|
261
|
+
color="var(--accent)"
|
|
262
|
+
showGrid
|
|
263
|
+
showDots
|
|
264
|
+
showArea
|
|
265
|
+
/>
|
|
266
|
+
</article>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
{/* Outcome Stats */}
|
|
270
|
+
<article>
|
|
271
|
+
<h4>{t('thinkingModels.outcomeStats')}</h4>
|
|
272
|
+
<div className="pill-row">
|
|
273
|
+
<span className="badge">{t('thinkingModels.success')} {formatPercent(detail.outcomeStats.successRate)}</span>
|
|
274
|
+
<span className="badge">{t('thinkingModels.failure')} {formatPercent(detail.outcomeStats.failureRate)}</span>
|
|
275
|
+
<span className="badge">{t('thinkingModels.pain')} {formatPercent(detail.outcomeStats.painRate)}</span>
|
|
276
|
+
<span className="badge">{t('thinkingModels.correction')} {formatPercent(detail.outcomeStats.correctionRate)}</span>
|
|
277
|
+
</div>
|
|
278
|
+
</article>
|
|
279
|
+
|
|
280
|
+
{/* Scenario Distribution */}
|
|
281
|
+
{detail.scenarioDistribution.length > 0 && (
|
|
282
|
+
<article>
|
|
283
|
+
<h4>{t('thinkingModels.scenarioDistribution')}</h4>
|
|
284
|
+
<div className="stack">
|
|
285
|
+
{detail.scenarioDistribution.map((item) => (
|
|
286
|
+
<div className="row-card" key={item.scenario}>
|
|
287
|
+
<strong>{item.scenario}</strong>
|
|
288
|
+
<span>{item.hits}</span>
|
|
289
|
+
</div>
|
|
290
|
+
))}
|
|
114
291
|
</div>
|
|
115
|
-
|
|
292
|
+
</article>
|
|
293
|
+
)}
|
|
294
|
+
|
|
295
|
+
{/* Recent Events */}
|
|
296
|
+
{detail.recentEvents.length > 0 && (
|
|
297
|
+
<article>
|
|
298
|
+
<h4>{t('thinkingModels.recentEvents')}</h4>
|
|
299
|
+
<div className="stack">
|
|
300
|
+
{detail.recentEvents.map((event) => (
|
|
301
|
+
<div className="row-card vertical" key={event.id}>
|
|
302
|
+
<div>
|
|
303
|
+
<strong>{formatDate(event.createdAt)}</strong>
|
|
304
|
+
<span>{event.scenarios.join(', ') || '—'}</span>
|
|
305
|
+
</div>
|
|
306
|
+
{(event as any).toolContext?.length > 0 && (
|
|
307
|
+
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)' }}>
|
|
308
|
+
🛠 {(event as any).toolContext.map((tc: any) => `${tc.toolName} (${tc.outcome})`).join(', ')}
|
|
309
|
+
</div>
|
|
310
|
+
)}
|
|
311
|
+
{(event as any).painContext?.length > 0 && (
|
|
312
|
+
<div style={{ fontSize: '0.7rem', color: 'var(--error)' }}>
|
|
313
|
+
⚡ {(event as any).painContext.map((pc: any) => `${pc.source} (${pc.score})`).join(', ')}
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
{(event as any).principleContext?.length > 0 && (
|
|
317
|
+
<div style={{ fontSize: '0.7rem', color: 'var(--info)' }}>
|
|
318
|
+
📋 {(event as any).principleContext.map((pr: any) => `${pr.principleId}`).join(', ')}
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
<pre style={{ fontSize: '0.7rem', whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
322
|
+
{event.triggerExcerpt}
|
|
323
|
+
</pre>
|
|
324
|
+
</div>
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
</article>
|
|
328
|
+
)}
|
|
329
|
+
|
|
330
|
+
{/* No data message for detail */}
|
|
331
|
+
{detail.usageTrend.length === 0 && detail.recentEvents.length === 0 && (
|
|
332
|
+
<div style={{ textAlign: 'center', padding: 'var(--space-4)', color: 'var(--text-secondary)' }}>
|
|
333
|
+
<Info size={20} style={{ marginBottom: 8 }} />
|
|
334
|
+
<p style={{ fontSize: '0.8rem' }}>No usage data for this model yet.</p>
|
|
116
335
|
</div>
|
|
117
|
-
)
|
|
336
|
+
)}
|
|
118
337
|
</div>
|
|
119
|
-
|
|
120
|
-
</
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
338
|
+
)}
|
|
339
|
+
</section>
|
|
340
|
+
</div>
|
|
341
|
+
</>
|
|
342
|
+
)}
|
|
124
343
|
</div>
|
|
125
344
|
);
|
|
126
345
|
}
|
|
127
|
-
|