flowmind 1.1.0 → 1.2.3
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/bin/flowmind.js +514 -0
- package/core/event-bus.js +17 -0
- package/core/honor-engine.js +255 -0
- package/core/index.js +42 -3
- package/core/learning-engine.js +29 -1
- package/core/skill-loader.js +9 -1
- package/dashboard/app.jsx +29 -0
- package/dashboard/components/ActivityFeed.jsx +86 -0
- package/dashboard/components/DragonPanel.jsx +67 -0
- package/dashboard/components/McpStatusBar.jsx +43 -0
- package/dashboard/components/StatsRow.jsx +65 -0
- package/mcp/server.js +63 -48
- package/package.json +11 -5
- package/tui/app.jsx +69 -0
- package/tui/components/ChatPanel.jsx +72 -0
- package/tui/components/DragonTotem.jsx +108 -0
- package/tui/components/ResultPanel.jsx +33 -0
- package/tui/components/Sidebar.jsx +70 -0
- package/tui/components/StatusBar.jsx +49 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, Text } = require('ink');
|
|
3
|
+
|
|
4
|
+
const LEVEL_NAMES = ['Egg', 'Hatchling', 'Juvenile', 'Adult', 'Elder', 'Ascended'];
|
|
5
|
+
|
|
6
|
+
function StatsRow({ flowmind }) {
|
|
7
|
+
const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
|
|
8
|
+
const [learningStats, setLearningStats] = React.useState({ totalRecords: 0, byType: {} });
|
|
9
|
+
const [aiStatus, setAiStatus] = React.useState({ initialized: false, defaultProvider: 'none' });
|
|
10
|
+
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
if (!flowmind) return;
|
|
13
|
+
const refresh = () => {
|
|
14
|
+
try { setHonorData(flowmind.getHonorData()); } catch (e) { /* ignore */ }
|
|
15
|
+
try { flowmind.getStats().then(s => setLearningStats(s)); } catch (e) { /* ignore */ }
|
|
16
|
+
try { setAiStatus(flowmind.getAIStatus()); } catch (e) { /* ignore */ }
|
|
17
|
+
};
|
|
18
|
+
refresh();
|
|
19
|
+
const interval = setInterval(refresh, 5000);
|
|
20
|
+
return () => clearInterval(interval);
|
|
21
|
+
}, [flowmind]);
|
|
22
|
+
|
|
23
|
+
const barWidth = 16;
|
|
24
|
+
const progress = honorData.points > 0 ? Math.min(1, honorData.points / 100) : 0;
|
|
25
|
+
const filled = Math.round(progress * barWidth);
|
|
26
|
+
const progressBar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
React.createElement(Box, { flexDirection: 'row' },
|
|
30
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'yellow', paddingX: 1, width: '33%' },
|
|
31
|
+
React.createElement(Text, { bold: true, color: 'yellow' }, 'Honor'),
|
|
32
|
+
React.createElement(Text, null,
|
|
33
|
+
React.createElement(Text, { color: 'yellow' }, LEVEL_NAMES[honorData.level] || 'Egg'),
|
|
34
|
+
React.createElement(Text, { color: 'gray' }, ' Lv' + honorData.level)
|
|
35
|
+
),
|
|
36
|
+
React.createElement(Text, { color: 'green' }, progressBar),
|
|
37
|
+
React.createElement(Text, { color: 'gray' }, honorData.points + '/100 pts')
|
|
38
|
+
),
|
|
39
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'cyan', paddingX: 1, width: '33%' },
|
|
40
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Learning'),
|
|
41
|
+
React.createElement(Text, null,
|
|
42
|
+
React.createElement(Text, { color: 'white' }, '' + (learningStats.totalRecords || 0)),
|
|
43
|
+
React.createElement(Text, { color: 'gray' }, ' records')
|
|
44
|
+
),
|
|
45
|
+
Object.entries(learningStats.byType || {}).map(([type, count]) =>
|
|
46
|
+
React.createElement(Text, { key: type, color: 'gray' }, ' ' + type + ': ' + count)
|
|
47
|
+
),
|
|
48
|
+
React.createElement(Text, null,
|
|
49
|
+
React.createElement(Text, { color: 'gray' }, 'AI: '),
|
|
50
|
+
React.createElement(Text, { color: aiStatus.initialized ? 'green' : 'red' }, aiStatus.initialized ? 'ok' : 'off')
|
|
51
|
+
)
|
|
52
|
+
),
|
|
53
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'blue', paddingX: 1, width: '33%' },
|
|
54
|
+
React.createElement(Text, { bold: true, color: 'blue' }, 'Components'),
|
|
55
|
+
React.createElement(Text, { color: 'gray' }, 'Registry loaded'),
|
|
56
|
+
React.createElement(Text, null,
|
|
57
|
+
React.createElement(Text, { color: 'gray' }, 'Provider: '),
|
|
58
|
+
React.createElement(Text, { color: aiStatus.initialized ? 'green' : 'red' }, aiStatus.defaultProvider || 'none')
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = StatsRow;
|
package/mcp/server.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const FlowMind = require('../core');
|
|
9
|
+
const eventBus = require('../core/event-bus');
|
|
9
10
|
|
|
10
11
|
// MCP Server 实现
|
|
11
12
|
class FlowMindMCPServer {
|
|
@@ -122,106 +123,120 @@ class FlowMindMCPServer {
|
|
|
122
123
|
async callTool(name, args) {
|
|
123
124
|
await this.init();
|
|
124
125
|
|
|
126
|
+
const callStart = Date.now();
|
|
127
|
+
let result;
|
|
128
|
+
|
|
125
129
|
try {
|
|
126
130
|
// 核心工具
|
|
127
131
|
if (name === 'flowmind_process') {
|
|
128
|
-
const
|
|
129
|
-
|
|
132
|
+
const data = await this.flowmind.process(args.input, args.context || {});
|
|
133
|
+
result = {
|
|
130
134
|
content: [{
|
|
131
135
|
type: 'text',
|
|
132
|
-
text: JSON.stringify(
|
|
136
|
+
text: JSON.stringify(data, null, 2)
|
|
133
137
|
}]
|
|
134
138
|
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (name === 'flowmind_list_skills') {
|
|
139
|
+
} else if (name === 'flowmind_list_skills') {
|
|
138
140
|
const skills = this.flowmind.skills.list();
|
|
139
|
-
|
|
141
|
+
result = {
|
|
140
142
|
content: [{
|
|
141
143
|
type: 'text',
|
|
142
144
|
text: JSON.stringify({ skills }, null, 2)
|
|
143
145
|
}]
|
|
144
146
|
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (name === 'flowmind_get_skill') {
|
|
147
|
+
} else if (name === 'flowmind_get_skill') {
|
|
148
148
|
const skill = this.flowmind.skills.get(args.name);
|
|
149
149
|
if (!skill) {
|
|
150
|
-
|
|
150
|
+
result = {
|
|
151
151
|
content: [{
|
|
152
152
|
type: 'text',
|
|
153
153
|
text: JSON.stringify({ error: `Skill not found: ${args.name}` })
|
|
154
154
|
}],
|
|
155
155
|
isError: true
|
|
156
156
|
};
|
|
157
|
+
} else {
|
|
158
|
+
result = {
|
|
159
|
+
content: [{
|
|
160
|
+
type: 'text',
|
|
161
|
+
text: JSON.stringify({
|
|
162
|
+
name: skill.name,
|
|
163
|
+
description: skill.definition?.description,
|
|
164
|
+
category: skill.definition?.category,
|
|
165
|
+
triggers: skill.definition?.triggers,
|
|
166
|
+
componentDependencies: skill.definition?.componentDependencies
|
|
167
|
+
}, null, 2)
|
|
168
|
+
}]
|
|
169
|
+
};
|
|
157
170
|
}
|
|
158
|
-
|
|
159
|
-
content: [{
|
|
160
|
-
type: 'text',
|
|
161
|
-
text: JSON.stringify({
|
|
162
|
-
name: skill.name,
|
|
163
|
-
description: skill.definition?.description,
|
|
164
|
-
category: skill.definition?.category,
|
|
165
|
-
triggers: skill.definition?.triggers,
|
|
166
|
-
componentDependencies: skill.definition?.componentDependencies
|
|
167
|
-
}, null, 2)
|
|
168
|
-
}]
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (name === 'flowmind_ai_status') {
|
|
171
|
+
} else if (name === 'flowmind_ai_status') {
|
|
173
172
|
const status = this.flowmind.getAIStatus();
|
|
174
|
-
|
|
173
|
+
result = {
|
|
175
174
|
content: [{
|
|
176
175
|
type: 'text',
|
|
177
176
|
text: JSON.stringify(status, null, 2)
|
|
178
177
|
}]
|
|
179
178
|
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (name === 'flowmind_learning_stats') {
|
|
179
|
+
} else if (name === 'flowmind_learning_stats') {
|
|
183
180
|
const stats = await this.flowmind.getStats();
|
|
184
|
-
|
|
181
|
+
result = {
|
|
185
182
|
content: [{
|
|
186
183
|
type: 'text',
|
|
187
184
|
text: JSON.stringify(stats, null, 2)
|
|
188
185
|
}]
|
|
189
186
|
};
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 技能工具
|
|
193
|
-
if (name.startsWith('flowmind_skill_')) {
|
|
187
|
+
} else if (name.startsWith('flowmind_skill_')) {
|
|
188
|
+
// 技能工具
|
|
194
189
|
const skillName = name.replace('flowmind_skill_', '');
|
|
195
190
|
const skill = this.flowmind.skills.get(skillName);
|
|
196
191
|
|
|
197
192
|
if (!skill) {
|
|
198
|
-
|
|
193
|
+
result = {
|
|
199
194
|
content: [{
|
|
200
195
|
type: 'text',
|
|
201
196
|
text: JSON.stringify({ error: `Skill not found: ${skillName}` })
|
|
202
197
|
}],
|
|
203
198
|
isError: true
|
|
204
199
|
};
|
|
200
|
+
} else {
|
|
201
|
+
const data = await this.flowmind.executeWithLearning(skill, args.input, args.context || {});
|
|
202
|
+
result = {
|
|
203
|
+
content: [{
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: JSON.stringify(data, null, 2)
|
|
206
|
+
}]
|
|
207
|
+
};
|
|
205
208
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
+
} else {
|
|
210
|
+
result = {
|
|
209
211
|
content: [{
|
|
210
212
|
type: 'text',
|
|
211
|
-
text: JSON.stringify(
|
|
212
|
-
}]
|
|
213
|
+
text: JSON.stringify({ error: `Unknown tool: ${name}` })
|
|
214
|
+
}],
|
|
215
|
+
isError: true
|
|
213
216
|
};
|
|
214
217
|
}
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
// Emit success event
|
|
220
|
+
eventBus.emit('mcp:tool_called', {
|
|
221
|
+
tool: name,
|
|
222
|
+
args,
|
|
223
|
+
duration: Date.now() - callStart,
|
|
224
|
+
success: !result.isError,
|
|
225
|
+
timestamp: new Date().toISOString()
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return result;
|
|
223
229
|
|
|
224
230
|
} catch (error) {
|
|
231
|
+
eventBus.emit('mcp:tool_called', {
|
|
232
|
+
tool: name,
|
|
233
|
+
args,
|
|
234
|
+
duration: Date.now() - callStart,
|
|
235
|
+
success: false,
|
|
236
|
+
error: error.message,
|
|
237
|
+
timestamp: new Date().toISOString()
|
|
238
|
+
});
|
|
239
|
+
|
|
225
240
|
return {
|
|
226
241
|
content: [{
|
|
227
242
|
type: 'text',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowmind",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "The AI Agent That Learns How You Work - Stop repeating yourself, FlowMind learns your workflows and applies them automatically.",
|
|
5
5
|
"main": "core/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
"core/",
|
|
44
44
|
"mcp/",
|
|
45
45
|
"skills/",
|
|
46
|
+
"tui/",
|
|
47
|
+
"dashboard/",
|
|
46
48
|
"scripts/",
|
|
47
49
|
"templates/",
|
|
48
50
|
"config/",
|
|
@@ -56,16 +58,20 @@
|
|
|
56
58
|
"node": ">=18.0.0"
|
|
57
59
|
},
|
|
58
60
|
"dependencies": {
|
|
59
|
-
"commander": "^11.0.0",
|
|
60
61
|
"chalk": "^4.1.2",
|
|
61
|
-
"
|
|
62
|
-
"inquirer": "^8.2.6",
|
|
62
|
+
"commander": "^11.0.0",
|
|
63
63
|
"fs-extra": "^11.1.1",
|
|
64
|
+
"ink": "^3.2.0",
|
|
65
|
+
"ink-spinner": "^4.0.3",
|
|
66
|
+
"ink-text-input": "^4.0.3",
|
|
67
|
+
"inquirer": "^8.2.6",
|
|
68
|
+
"ora": "^5.4.1",
|
|
69
|
+
"react": "^18.3.1",
|
|
64
70
|
"uuid": "^9.0.0"
|
|
65
71
|
},
|
|
66
72
|
"devDependencies": {
|
|
67
|
-
"jest": "^29.7.0",
|
|
68
73
|
"eslint": "^8.50.0",
|
|
74
|
+
"jest": "^29.7.0",
|
|
69
75
|
"nodemon": "^3.0.1",
|
|
70
76
|
"webpack": "^5.88.0",
|
|
71
77
|
"webpack-cli": "^5.1.4"
|
package/tui/app.jsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, Text, useApp, useInput } = require('ink');
|
|
3
|
+
const Sidebar = require('./components/Sidebar.jsx');
|
|
4
|
+
const ChatPanel = require('./components/ChatPanel.jsx');
|
|
5
|
+
const ResultPanel = require('./components/ResultPanel.jsx');
|
|
6
|
+
const StatusBar = require('./components/StatusBar.jsx');
|
|
7
|
+
|
|
8
|
+
function App({ flowmind }) {
|
|
9
|
+
const [results, setResults] = React.useState([]);
|
|
10
|
+
const [isProcessing, setIsProcessing] = React.useState(false);
|
|
11
|
+
const mountedRef = React.useRef(true);
|
|
12
|
+
const { exit } = useApp();
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
return () => { mountedRef.current = false; };
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
useInput((input, key) => {
|
|
19
|
+
if (key.ctrl && input === 'c') exit();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const handleCommand = React.useCallback(async (input, addResponse) => {
|
|
23
|
+
setIsProcessing(true);
|
|
24
|
+
try {
|
|
25
|
+
const result = await flowmind.process(input);
|
|
26
|
+
if (!mountedRef.current) return;
|
|
27
|
+
if (result.type === 'result') {
|
|
28
|
+
const text = typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2);
|
|
29
|
+
addResponse(text.substring(0, 200) + (text.length > 200 ? '...' : ''));
|
|
30
|
+
} else if (result.type === 'learning') {
|
|
31
|
+
addResponse(result.message || 'Learning recorded');
|
|
32
|
+
} else if (result.type === 'error') {
|
|
33
|
+
addResponse('Error: ' + result.message);
|
|
34
|
+
}
|
|
35
|
+
setResults(prev => [...prev, result]);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (mountedRef.current) addResponse('Error: ' + e.message);
|
|
38
|
+
} finally {
|
|
39
|
+
if (mountedRef.current) setIsProcessing(false);
|
|
40
|
+
}
|
|
41
|
+
}, [flowmind]);
|
|
42
|
+
|
|
43
|
+
const handleSkillSelect = React.useCallback((skill) => {
|
|
44
|
+
try {
|
|
45
|
+
setResults(prev => [...prev, {
|
|
46
|
+
type: 'result',
|
|
47
|
+
data: { name: skill.name, description: skill.definition?.description || 'No description', category: skill.category || 'general', path: skill.path },
|
|
48
|
+
metadata: { skill: skill.name }
|
|
49
|
+
}]);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// ignore skill select errors
|
|
52
|
+
}
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
React.createElement(Box, { flexDirection: 'column', width: '100%', height: '100%' },
|
|
57
|
+
React.createElement(Box, { flexDirection: 'row', flexGrow: 1 },
|
|
58
|
+
React.createElement(Sidebar, { flowmind: flowmind, width: 30, onSkillSelect: handleSkillSelect }),
|
|
59
|
+
React.createElement(Box, { flexDirection: 'column', width: '70%', flexGrow: 1 },
|
|
60
|
+
React.createElement(ChatPanel, { onSubmit: handleCommand, isProcessing: isProcessing, onExit: exit }),
|
|
61
|
+
React.createElement(ResultPanel, { results: results })
|
|
62
|
+
)
|
|
63
|
+
),
|
|
64
|
+
React.createElement(StatusBar, { flowmind: flowmind })
|
|
65
|
+
)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = App;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, Text } = require('ink');
|
|
3
|
+
const TextInput = require('ink-text-input').default || require('ink-text-input');
|
|
4
|
+
const Spinner = require('ink-spinner').default || require('ink-spinner');
|
|
5
|
+
|
|
6
|
+
function ChatPanel({ onSubmit, isProcessing, onExit }) {
|
|
7
|
+
const [input, setInput] = React.useState('');
|
|
8
|
+
const [history, setHistory] = React.useState([]);
|
|
9
|
+
const mountedRef = React.useRef(true);
|
|
10
|
+
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
return () => { mountedRef.current = false; };
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
const handleSubmit = (value) => {
|
|
16
|
+
if (!value.trim()) return;
|
|
17
|
+
setHistory(prev => [...prev, { role: 'user', text: value }]);
|
|
18
|
+
setInput('');
|
|
19
|
+
if (value.toLowerCase() === 'exit' || value.toLowerCase() === 'quit') {
|
|
20
|
+
if (onExit) onExit();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
onSubmit(value, (response) => {
|
|
25
|
+
if (mountedRef.current) {
|
|
26
|
+
setHistory(prev => [...prev, { role: 'flowmind', text: response }]);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
} catch (e) {
|
|
30
|
+
if (mountedRef.current) {
|
|
31
|
+
setHistory(prev => [...prev, { role: 'flowmind', text: 'Error: ' + e.message }]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const displayHistory = history.slice(-20);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'green', paddingX: 1 },
|
|
40
|
+
React.createElement(Text, { bold: true, color: 'green' }, 'Command Input'),
|
|
41
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1, minHeight: 6 },
|
|
42
|
+
displayHistory.length === 0 && React.createElement(Text, { color: 'gray' }, 'Type a command to get started. Type "exit" to quit.'),
|
|
43
|
+
displayHistory.map((msg, i) =>
|
|
44
|
+
React.createElement(Box, { key: i, flexDirection: 'column' },
|
|
45
|
+
msg.role === 'user'
|
|
46
|
+
? React.createElement(Text, null,
|
|
47
|
+
React.createElement(Text, { color: 'green', bold: true }, '> '),
|
|
48
|
+
React.createElement(Text, { color: 'white' }, msg.text)
|
|
49
|
+
)
|
|
50
|
+
: React.createElement(Text, null,
|
|
51
|
+
React.createElement(Text, { color: 'cyan', bold: true }, '< '),
|
|
52
|
+
React.createElement(Text, { color: 'cyan' }, msg.text)
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
),
|
|
57
|
+
React.createElement(Box, { marginTop: 1 },
|
|
58
|
+
isProcessing
|
|
59
|
+
? React.createElement(Text, { color: 'yellow' },
|
|
60
|
+
React.createElement(Spinner, { type: 'dots' }),
|
|
61
|
+
' Processing...'
|
|
62
|
+
)
|
|
63
|
+
: React.createElement(Box, null,
|
|
64
|
+
React.createElement(Text, { color: 'green', bold: true }, '> '),
|
|
65
|
+
React.createElement(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit })
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = ChatPanel;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, Text } = require('ink');
|
|
3
|
+
|
|
4
|
+
const DRAGON_ARTS = {
|
|
5
|
+
0: [
|
|
6
|
+
' ╭─────╮ ',
|
|
7
|
+
' ╱ ╭─╮ ╲ ',
|
|
8
|
+
' │ │ │ │ ',
|
|
9
|
+
' │ │ ◎ │ │ ',
|
|
10
|
+
' │ ╰─╯ │ ',
|
|
11
|
+
' ╲ ╱ ',
|
|
12
|
+
' ╰─────╯ ',
|
|
13
|
+
],
|
|
14
|
+
1: [
|
|
15
|
+
' ╭──╮ ',
|
|
16
|
+
' ╭────╯ ╰───╮ ',
|
|
17
|
+
' ╱ ◎ ╰─╯ ╲ ',
|
|
18
|
+
' ╱ ▽ ╲ ',
|
|
19
|
+
' ╲ ╱╲ ╱╲ ╱ ',
|
|
20
|
+
' ╲╱╱ ╲╱╱ ╲╱╲╱ ',
|
|
21
|
+
],
|
|
22
|
+
2: [
|
|
23
|
+
' ╭─╮ ╭─╮ ',
|
|
24
|
+
' ╭────╯ ╰──╯ ╰───╮ ',
|
|
25
|
+
' ╱ ◎ ╰──╯ ╲ ',
|
|
26
|
+
' ╱ ╭────────╮ ╲ ',
|
|
27
|
+
' ╲ ╱ ╱╱╱╱╱╱╱╱ ╲ ╱ ',
|
|
28
|
+
' ╲───╯ ╱╱╱╱╱╱╱╱╱╱ ╰──╱ ',
|
|
29
|
+
' ╰─╯ ╰─╯ ',
|
|
30
|
+
],
|
|
31
|
+
3: [
|
|
32
|
+
' ╭───╮ ╭───╮ ',
|
|
33
|
+
' ╭───╯ ╰──╯ ╰───╮ ',
|
|
34
|
+
' ╱ ◎ ╰───╯ ╲ ',
|
|
35
|
+
'│ ╭──────────╮ │ ',
|
|
36
|
+
'│ ╱ ╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
|
|
37
|
+
' ╲──╯ ╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
|
|
38
|
+
' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╱ ',
|
|
39
|
+
' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
|
|
40
|
+
' ╰───╯ ╰───╯ ',
|
|
41
|
+
],
|
|
42
|
+
4: [
|
|
43
|
+
' ╭───╮ ╭───╮ ',
|
|
44
|
+
'╭───╯ ╰──────╯ ╰───╮ ',
|
|
45
|
+
'│ ◎ ╰───╯ │ ',
|
|
46
|
+
'│ ╭────────────╮ │ ',
|
|
47
|
+
'│ ╱ ╱╱╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
|
|
48
|
+
' ╲───╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰──╯ ',
|
|
49
|
+
' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╲ ',
|
|
50
|
+
' ╲─╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰─╲ ',
|
|
51
|
+
' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
|
|
52
|
+
' ╰───╯ ╰───╯ ',
|
|
53
|
+
],
|
|
54
|
+
5: [
|
|
55
|
+
' ★ ╭───╮ ╭───╮ ★ ',
|
|
56
|
+
'╭─╯ ╰──╯ ╰──╯ ╰─╮ ',
|
|
57
|
+
'│ ◎ ╰───╯ │ ',
|
|
58
|
+
'│ ╭──────────────╮ │ ',
|
|
59
|
+
'│ ╱ ★╱╱╱╱╱╱╱╱╱╱★╱╱ ╲ │ ',
|
|
60
|
+
' ╲────╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
|
|
61
|
+
' ╲ ╱╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱ ╲ ',
|
|
62
|
+
' ╲──╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰──╲ ',
|
|
63
|
+
' ╲─╯╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱╰──╲ ',
|
|
64
|
+
' ★ ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ★ ',
|
|
65
|
+
' ╰───╯ ╰───╯ ',
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const LEVEL_NAMES = ['Egg', 'Hatchling', 'Juvenile', 'Adult', 'Elder', 'Ascended'];
|
|
70
|
+
const LEVEL_STATES = ['dormant', 'awakening', 'growing', 'soaring', 'wise', 'transcendent'];
|
|
71
|
+
const LEVEL_COLORS = ['gray', 'cyan', 'cyan', 'cyanBright', 'cyanBright', 'cyanBright'];
|
|
72
|
+
|
|
73
|
+
function DragonTotem({ honorData, compact }) {
|
|
74
|
+
const points = honorData?.points || 0;
|
|
75
|
+
const level = honorData?.level || 0;
|
|
76
|
+
const art = DRAGON_ARTS[level] || DRAGON_ARTS[0];
|
|
77
|
+
const color = LEVEL_COLORS[level] || 'gray';
|
|
78
|
+
const levelName = LEVEL_NAMES[level] || 'Unknown';
|
|
79
|
+
const state = LEVEL_STATES[level] || 'unknown';
|
|
80
|
+
const nextLevelPoints = [1, 10, 30, 60, 100];
|
|
81
|
+
const nextPoints = nextLevelPoints[level] || null;
|
|
82
|
+
const pointsToNext = nextPoints !== null ? nextPoints - points : 0;
|
|
83
|
+
const lines = compact ? art.slice(0, 4) : art;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
React.createElement(Box, { flexDirection: 'column', paddingX: 1 },
|
|
87
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Dragon Totem'),
|
|
88
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
89
|
+
lines.map((line, i) => React.createElement(Text, { key: i, color: color }, line))
|
|
90
|
+
),
|
|
91
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
92
|
+
React.createElement(Text, null,
|
|
93
|
+
React.createElement(Text, { color: 'yellow' }, 'Lv' + level),
|
|
94
|
+
React.createElement(Text, { color: 'white' }, ' ' + levelName),
|
|
95
|
+
React.createElement(Text, { color: 'gray' }, ' (' + state + ')')
|
|
96
|
+
),
|
|
97
|
+
React.createElement(Text, null,
|
|
98
|
+
React.createElement(Text, { color: 'yellow' }, '' + points),
|
|
99
|
+
React.createElement(Text, { color: 'gray' }, ' pts')
|
|
100
|
+
),
|
|
101
|
+
pointsToNext > 0 && React.createElement(Text, { color: 'gray' }, pointsToNext + ' pts to ' + LEVEL_NAMES[level + 1]),
|
|
102
|
+
pointsToNext <= 0 && level >= 5 && React.createElement(Text, { color: 'yellow' }, 'Max level!')
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = DragonTotem;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, Text } = require('ink');
|
|
3
|
+
|
|
4
|
+
function ResultPanel({ results }) {
|
|
5
|
+
const displayResults = results.slice(-30);
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
React.createElement(Box, { flexDirection: 'column', borderStyle: 'single', borderColor: 'yellow', paddingX: 1, flexGrow: 1 },
|
|
9
|
+
React.createElement(Text, { bold: true, color: 'yellow' }, 'Results'),
|
|
10
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1, overflow: 'hidden' },
|
|
11
|
+
displayResults.length === 0 && React.createElement(Text, { color: 'gray' }, 'No results yet. Execute a command to see output here.'),
|
|
12
|
+
displayResults.map((result, i) =>
|
|
13
|
+
React.createElement(Box, { key: i, flexDirection: 'column', marginBottom: 1 },
|
|
14
|
+
result.type === 'result' && React.createElement(React.Fragment, null,
|
|
15
|
+
result.metadata?.skill && React.createElement(Text, null,
|
|
16
|
+
React.createElement(Text, { color: 'gray' }, 'Skill: '),
|
|
17
|
+
React.createElement(Text, { color: 'green' }, result.metadata.skill),
|
|
18
|
+
result.metadata?.duration && React.createElement(Text, { color: 'gray' }, ' (' + result.metadata.duration + 'ms)')
|
|
19
|
+
),
|
|
20
|
+
React.createElement(Text, { color: 'white' },
|
|
21
|
+
typeof result.data === 'string' ? result.data : JSON.stringify(result.data, null, 2)
|
|
22
|
+
)
|
|
23
|
+
),
|
|
24
|
+
result.type === 'learning' && React.createElement(Text, { color: 'cyan' }, result.message),
|
|
25
|
+
result.type === 'error' && React.createElement(Text, { color: 'red' }, 'Error: ' + result.message)
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = ResultPanel;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { Box, Text, useInput } = require('ink');
|
|
3
|
+
const DragonTotem = require('./DragonTotem.jsx');
|
|
4
|
+
|
|
5
|
+
function Sidebar({ flowmind, width, onSkillSelect }) {
|
|
6
|
+
const [selectedIndex, setSelectedIndex] = React.useState(0);
|
|
7
|
+
const [skills, setSkills] = React.useState([]);
|
|
8
|
+
const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
|
|
9
|
+
|
|
10
|
+
React.useEffect(() => {
|
|
11
|
+
if (flowmind) {
|
|
12
|
+
try {
|
|
13
|
+
const list = flowmind.skills.list() || [];
|
|
14
|
+
setSkills(list);
|
|
15
|
+
} catch (e) {
|
|
16
|
+
setSkills([]);
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
setHonorData(flowmind.getHonorData());
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// keep default honorData
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}, [flowmind]);
|
|
25
|
+
|
|
26
|
+
useInput((input, key) => {
|
|
27
|
+
if (key.upArrow) setSelectedIndex(prev => Math.max(0, prev - 1));
|
|
28
|
+
else if (key.downArrow) setSelectedIndex(prev => Math.min(skills.length - 1, prev + 1));
|
|
29
|
+
else if (key.return && skills[selectedIndex] && onSkillSelect) onSkillSelect(skills[selectedIndex]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const barWidth = Math.max(10, width - 4);
|
|
33
|
+
const progress = honorData.points > 0 ? Math.min(1, honorData.points / 100) : 0;
|
|
34
|
+
const filled = Math.round(progress * barWidth);
|
|
35
|
+
const progressBar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
React.createElement(Box, { flexDirection: 'column', width: width, borderStyle: 'single', borderColor: 'cyan', paddingX: 1 },
|
|
39
|
+
React.createElement(DragonTotem, { honorData: honorData, compact: true }),
|
|
40
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
41
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Progress'),
|
|
42
|
+
React.createElement(Text, { color: 'green' }, progressBar),
|
|
43
|
+
React.createElement(Text, { color: 'gray' }, honorData.points + '/100 pts')
|
|
44
|
+
),
|
|
45
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
46
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Skills (' + skills.length + ')'),
|
|
47
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
48
|
+
skills.length === 0 && React.createElement(Text, { color: 'gray' }, 'No skills loaded'),
|
|
49
|
+
skills.map((skill, i) => {
|
|
50
|
+
const isSelected = i === selectedIndex;
|
|
51
|
+
const category = skill.category || 'general';
|
|
52
|
+
const prefix = isSelected ? '\u25B6 ' : ' ';
|
|
53
|
+
return React.createElement(Text, { key: skill.name },
|
|
54
|
+
React.createElement(Text, { color: isSelected ? 'green' : 'white' }, prefix + skill.name),
|
|
55
|
+
React.createElement(Text, { color: 'gray' }, ' [' + category + ']')
|
|
56
|
+
);
|
|
57
|
+
})
|
|
58
|
+
)
|
|
59
|
+
),
|
|
60
|
+
React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
|
|
61
|
+
React.createElement(Text, { bold: true, color: 'cyan' }, 'Stats'),
|
|
62
|
+
React.createElement(Text, { color: 'gray' }, 'Skills used: ' + (honorData.stats?.skillUseCount || 0)),
|
|
63
|
+
React.createElement(Text, { color: 'gray' }, 'Learnings: ' + (honorData.stats?.learningCount || 0)),
|
|
64
|
+
React.createElement(Text, { color: 'gray' }, 'New skills: ' + (honorData.stats?.newSkillCount || 0))
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = Sidebar;
|