@projectservan8n/cnapse 0.2.1 → 0.5.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.
@@ -1,155 +1,253 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useState, useCallback } from 'react';
2
2
  import { Box, Text, useApp, useInput } from 'ink';
3
3
  import { Header } from './Header.js';
4
4
  import { ChatMessage } from './ChatMessage.js';
5
5
  import { ChatInput } from './ChatInput.js';
6
6
  import { StatusBar } from './StatusBar.js';
7
- import { chat, Message } from '../lib/api.js';
8
-
9
- interface ChatMsg {
10
- id: string;
11
- role: 'user' | 'assistant' | 'system';
12
- content: string;
13
- timestamp: Date;
14
- isStreaming?: boolean;
15
- }
7
+ import { HelpMenu } from './HelpMenu.js';
8
+ import { ProviderSelector } from './ProviderSelector.js';
9
+ import { getConfig } from '../lib/config.js';
10
+ import { useChat, useVision, useTelegram, useTasks } from '../hooks/index.js';
11
+
12
+ type OverlayType = 'none' | 'help' | 'provider';
16
13
 
17
14
  export function App() {
18
15
  const { exit } = useApp();
19
- const [messages, setMessages] = useState<ChatMsg[]>([
20
- {
21
- id: '0',
22
- role: 'system',
23
- content: 'Welcome to C-napse! Type your message and press Enter.\n\nShortcuts:\n Ctrl+C - Exit\n /clear - Clear chat\n /help - Show help',
24
- timestamp: new Date(),
25
- },
26
- ]);
27
- const [input, setInput] = useState('');
28
- const [isProcessing, setIsProcessing] = useState(false);
16
+
17
+ // UI State
18
+ const [overlay, setOverlay] = useState<OverlayType>('none');
19
+ const [screenWatch, setScreenWatch] = useState(false);
29
20
  const [status, setStatus] = useState('Ready');
30
- const [error, setError] = useState<string | null>(null);
31
21
 
22
+ // Feature hooks
23
+ const chat = useChat(screenWatch);
24
+ const vision = useVision();
25
+ const telegram = useTelegram((msg) => {
26
+ chat.addSystemMessage(`📱 Telegram [${msg.from}]: ${msg.text}`);
27
+ });
28
+ const tasks = useTasks((task, step) => {
29
+ if (step.status === 'running') {
30
+ setStatus(`Running: ${step.description}`);
31
+ }
32
+ });
33
+
34
+ // Keyboard shortcuts
32
35
  useInput((inputChar, key) => {
33
- if (key.ctrl && inputChar === 'c') {
34
- exit();
36
+ if (overlay !== 'none') return;
37
+
38
+ if (key.ctrl && inputChar === 'c') exit();
39
+ if (key.ctrl && inputChar === 'l') chat.clearMessages();
40
+ if (key.ctrl && inputChar === 'h') setOverlay('help');
41
+ if (key.ctrl && inputChar === 'p') setOverlay('provider');
42
+ if (key.ctrl && inputChar === 'w') {
43
+ setScreenWatch(prev => {
44
+ const newState = !prev;
45
+ chat.addSystemMessage(newState
46
+ ? '🖥️ Screen watching enabled.'
47
+ : '🖥️ Screen watching disabled.'
48
+ );
49
+ return newState;
50
+ });
35
51
  }
36
- if (key.ctrl && inputChar === 'l') {
37
- setMessages([messages[0]!]); // Keep welcome message
38
- setError(null);
52
+ if (key.ctrl && inputChar === 't') {
53
+ handleTelegramToggle();
39
54
  }
40
55
  });
41
56
 
42
- const handleSubmit = async (value: string) => {
43
- if (!value.trim() || isProcessing) return;
57
+ // Command handlers
58
+ const handleCommand = useCallback(async (cmd: string) => {
59
+ const parts = cmd.slice(1).split(' ');
60
+ const command = parts[0];
61
+ const args = parts.slice(1).join(' ');
44
62
 
45
- const userInput = value.trim();
46
- setInput('');
47
- setError(null);
63
+ switch (command) {
64
+ case 'clear':
65
+ chat.clearMessages();
66
+ chat.addSystemMessage('Chat cleared.');
67
+ break;
48
68
 
49
- // Handle commands
50
- if (userInput.startsWith('/')) {
51
- handleCommand(userInput);
52
- return;
69
+ case 'help':
70
+ setOverlay('help');
71
+ break;
72
+
73
+ case 'provider':
74
+ case 'model':
75
+ setOverlay('provider');
76
+ break;
77
+
78
+ case 'config': {
79
+ const config = getConfig();
80
+ chat.addSystemMessage(
81
+ `⚙️ Configuration:\n` +
82
+ ` Provider: ${config.provider}\n` +
83
+ ` Model: ${config.model}\n` +
84
+ ` Ollama: ${config.ollamaHost}\n\n` +
85
+ `Use /provider to change`
86
+ );
87
+ break;
88
+ }
89
+
90
+ case 'screen':
91
+ await handleScreenCommand();
92
+ break;
93
+
94
+ case 'watch':
95
+ setScreenWatch(prev => {
96
+ const newState = !prev;
97
+ chat.addSystemMessage(newState
98
+ ? '🖥️ Screen watching enabled.'
99
+ : '🖥️ Screen watching disabled.'
100
+ );
101
+ return newState;
102
+ });
103
+ break;
104
+
105
+ case 'telegram':
106
+ await handleTelegramToggle();
107
+ break;
108
+
109
+ case 'task':
110
+ if (args) {
111
+ await handleTaskCommand(args);
112
+ } else {
113
+ chat.addSystemMessage('Usage: /task <description>\nExample: /task open notepad and type hello');
114
+ }
115
+ break;
116
+
117
+ case 'memory':
118
+ if (args === 'clear') {
119
+ tasks.clearMemory();
120
+ chat.addSystemMessage('🧠 Task memory cleared.');
121
+ } else {
122
+ const stats = tasks.getMemoryStats();
123
+ chat.addSystemMessage(
124
+ `🧠 Task Memory:\n\n` +
125
+ ` Learned patterns: ${stats.patternCount}\n` +
126
+ ` Total successful uses: ${stats.totalUses}\n\n` +
127
+ (stats.topPatterns.length > 0
128
+ ? ` Top patterns:\n${stats.topPatterns.map(p => ` • ${p}`).join('\n')}\n\n`
129
+ : ' No patterns learned yet.\n\n') +
130
+ `The more you use /task, the smarter it gets!\n` +
131
+ `Use /memory clear to reset.`
132
+ );
133
+ }
134
+ break;
135
+
136
+ case 'quit':
137
+ case 'exit':
138
+ exit();
139
+ break;
140
+
141
+ default:
142
+ chat.addSystemMessage(`Unknown command: ${command}\nType /help for commands`);
53
143
  }
144
+ }, [chat, exit]);
54
145
 
55
- // Add user message
56
- const userMsg: ChatMsg = {
57
- id: Date.now().toString(),
58
- role: 'user',
59
- content: userInput,
60
- timestamp: new Date(),
61
- };
62
- setMessages((prev) => [...prev, userMsg]);
63
-
64
- // Add placeholder for assistant
65
- const assistantId = (Date.now() + 1).toString();
66
- setMessages((prev) => [
67
- ...prev,
68
- {
69
- id: assistantId,
70
- role: 'assistant',
71
- content: '',
72
- timestamp: new Date(),
73
- isStreaming: true,
74
- },
75
- ]);
76
-
77
- setIsProcessing(true);
78
- setStatus('Thinking...');
146
+ // Screen command
147
+ const handleScreenCommand = useCallback(async () => {
148
+ chat.addSystemMessage('📸 Analyzing screen...');
149
+ setStatus('Analyzing...');
79
150
 
80
151
  try {
81
- // Build message history for API
82
- const apiMessages: Message[] = messages
83
- .filter((m) => m.role === 'user' || m.role === 'assistant')
84
- .slice(-10)
85
- .map((m) => ({ role: m.role as 'user' | 'assistant', content: m.content }));
86
-
87
- apiMessages.push({ role: 'user', content: userInput });
88
-
89
- const response = await chat(apiMessages);
90
-
91
- // Update assistant message with response
92
- setMessages((prev) =>
93
- prev.map((m) =>
94
- m.id === assistantId
95
- ? { ...m, content: response.content || '(no response)', isStreaming: false }
96
- : m
97
- )
98
- );
152
+ const description = await vision.analyze();
153
+ chat.addSystemMessage(`🖥️ Screen:\n\n${description}`);
99
154
  } catch (err) {
100
- const errorMsg = err instanceof Error ? err.message : 'Unknown error';
101
- setError(errorMsg);
102
- // Update assistant message with error
103
- setMessages((prev) =>
104
- prev.map((m) =>
105
- m.id === assistantId
106
- ? { ...m, content: `Error: ${errorMsg}`, isStreaming: false }
107
- : m
108
- )
155
+ chat.addSystemMessage(`❌ ${vision.error || 'Vision failed'}`);
156
+ } finally {
157
+ setStatus('Ready');
158
+ }
159
+ }, [chat, vision]);
160
+
161
+ // Telegram toggle
162
+ const handleTelegramToggle = useCallback(async () => {
163
+ if (telegram.isEnabled) {
164
+ await telegram.stop();
165
+ chat.addSystemMessage('📱 Telegram stopped.');
166
+ } else {
167
+ chat.addSystemMessage('📱 Starting Telegram...');
168
+ setStatus('Starting Telegram...');
169
+ try {
170
+ await telegram.start();
171
+ chat.addSystemMessage(
172
+ '📱 Telegram started!\n' +
173
+ 'Send /start to your bot to connect.\n' +
174
+ 'Commands: /screen, /describe, /run, /status'
175
+ );
176
+ } catch {
177
+ chat.addSystemMessage(`❌ ${telegram.error || 'Telegram failed'}`);
178
+ } finally {
179
+ setStatus('Ready');
180
+ }
181
+ }
182
+ }, [chat, telegram]);
183
+
184
+ // Task command
185
+ const handleTaskCommand = useCallback(async (description: string) => {
186
+ chat.addSystemMessage(`📋 Parsing: ${description}`);
187
+ setStatus('Parsing task...');
188
+
189
+ try {
190
+ const task = await tasks.run(description);
191
+ chat.addSystemMessage(`\n${tasks.format(task)}`);
192
+ chat.addSystemMessage(task.status === 'completed'
193
+ ? '✅ Task completed!'
194
+ : '❌ Task failed.'
109
195
  );
196
+ } catch {
197
+ chat.addSystemMessage(`❌ ${tasks.error || 'Task failed'}`);
110
198
  } finally {
111
- setIsProcessing(false);
112
199
  setStatus('Ready');
113
200
  }
114
- };
201
+ }, [chat, tasks]);
115
202
 
116
- const handleCommand = (cmd: string) => {
117
- const parts = cmd.slice(1).split(' ');
118
- const command = parts[0];
203
+ // Submit handler
204
+ const handleSubmit = useCallback(async (value: string) => {
205
+ if (!value.trim()) return;
119
206
 
120
- switch (command) {
121
- case 'clear':
122
- setMessages([messages[0]!]);
123
- addSystemMessage('Chat cleared.');
124
- break;
125
- case 'help':
126
- addSystemMessage(
127
- 'Commands:\n /clear - Clear chat history\n /help - Show this help\n\nJust type naturally to chat with the AI!'
128
- );
129
- break;
130
- default:
131
- addSystemMessage(`Unknown command: ${command}`);
207
+ if (value.startsWith('/')) {
208
+ await handleCommand(value);
209
+ } else {
210
+ setStatus('Thinking...');
211
+ await chat.sendMessage(value);
212
+ setStatus('Ready');
132
213
  }
133
- };
134
-
135
- const addSystemMessage = (content: string) => {
136
- setMessages((prev) => [
137
- ...prev,
138
- {
139
- id: Date.now().toString(),
140
- role: 'system',
141
- content,
142
- timestamp: new Date(),
143
- },
144
- ]);
145
- };
146
-
147
- // Only show last N messages that fit
148
- const visibleMessages = messages.slice(-20);
214
+ }, [chat, handleCommand]);
215
+
216
+ // Provider selection callback
217
+ const handleProviderSelect = useCallback((provider: string, model: string) => {
218
+ chat.addSystemMessage(`✅ Updated: ${provider} / ${model}`);
219
+ }, [chat]);
220
+
221
+ // Render overlays
222
+ if (overlay === 'help') {
223
+ return (
224
+ <Box flexDirection="column" height="100%" alignItems="center" justifyContent="center">
225
+ <HelpMenu
226
+ onClose={() => setOverlay('none')}
227
+ onSelect={(cmd) => { setOverlay('none'); handleCommand(cmd); }}
228
+ />
229
+ </Box>
230
+ );
231
+ }
232
+
233
+ if (overlay === 'provider') {
234
+ return (
235
+ <Box flexDirection="column" height="100%" alignItems="center" justifyContent="center">
236
+ <ProviderSelector
237
+ onClose={() => setOverlay('none')}
238
+ onSelect={handleProviderSelect}
239
+ />
240
+ </Box>
241
+ );
242
+ }
243
+
244
+ // Main UI
245
+ const visibleMessages = chat.messages.slice(-20);
246
+ const isProcessing = chat.isProcessing || vision.isAnalyzing || tasks.isRunning || telegram.isStarting;
149
247
 
150
248
  return (
151
249
  <Box flexDirection="column" height="100%">
152
- <Header />
250
+ <Header screenWatch={screenWatch} telegramEnabled={telegram.isEnabled} />
153
251
 
154
252
  <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="gray" padding={1}>
155
253
  <Text bold color="gray"> Chat </Text>
@@ -164,15 +262,15 @@ export function App() {
164
262
  ))}
165
263
  </Box>
166
264
 
167
- {error && (
265
+ {chat.error && (
168
266
  <Box marginY={1}>
169
- <Text color="red">Error: {error}</Text>
267
+ <Text color="red">Error: {chat.error}</Text>
170
268
  </Box>
171
269
  )}
172
270
 
173
271
  <ChatInput
174
- value={input}
175
- onChange={setInput}
272
+ value=""
273
+ onChange={() => {}}
176
274
  onSubmit={handleSubmit}
177
275
  isProcessing={isProcessing}
178
276
  />
@@ -11,7 +11,12 @@ const ASCII_BANNER = `
11
11
  ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚══════╝╚══════╝
12
12
  `.trim();
13
13
 
14
- export function Header() {
14
+ interface HeaderProps {
15
+ screenWatch?: boolean;
16
+ telegramEnabled?: boolean;
17
+ }
18
+
19
+ export function Header({ screenWatch = false, telegramEnabled = false }: HeaderProps) {
15
20
  const config = getConfig();
16
21
 
17
22
  return (
@@ -20,8 +25,13 @@ export function Header() {
20
25
  <Box justifyContent="center">
21
26
  <Text color="gray">
22
27
  {config.provider} │ {config.model}
28
+ {screenWatch && <Text color="yellow"> │ 🖥️ Screen</Text>}
29
+ {telegramEnabled && <Text color="blue"> │ 📱 Telegram</Text>}
23
30
  </Text>
24
31
  </Box>
32
+ <Box justifyContent="center">
33
+ <Text color="gray" dimColor>Press Ctrl+H for help</Text>
34
+ </Box>
25
35
  </Box>
26
36
  );
27
37
  }
@@ -0,0 +1,144 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+
4
+ interface MenuItem {
5
+ command: string;
6
+ shortcut?: string;
7
+ description: string;
8
+ category: 'navigation' | 'actions' | 'settings';
9
+ }
10
+
11
+ const MENU_ITEMS: MenuItem[] = [
12
+ // Navigation
13
+ { command: '/help', shortcut: 'Ctrl+H', description: 'Show this help menu', category: 'navigation' },
14
+ { command: '/clear', shortcut: 'Ctrl+L', description: 'Clear chat history', category: 'navigation' },
15
+ { command: '/quit', shortcut: 'Ctrl+C', description: 'Exit C-napse', category: 'navigation' },
16
+
17
+ // Actions
18
+ { command: '/screen', shortcut: 'Ctrl+S', description: 'Take screenshot and describe', category: 'actions' },
19
+ { command: '/task', description: 'Run multi-step task', category: 'actions' },
20
+ { command: '/telegram', shortcut: 'Ctrl+T', description: 'Toggle Telegram bot', category: 'actions' },
21
+ { command: '/memory', description: 'View/clear learned task patterns', category: 'actions' },
22
+
23
+ // Settings
24
+ { command: '/config', description: 'Show/edit configuration', category: 'settings' },
25
+ { command: '/watch', shortcut: 'Ctrl+W', description: 'Toggle screen watching', category: 'settings' },
26
+ { command: '/model', description: 'Change AI model', category: 'settings' },
27
+ { command: '/provider', shortcut: 'Ctrl+P', description: 'Change AI provider', category: 'settings' },
28
+ ];
29
+
30
+ interface HelpMenuProps {
31
+ onClose: () => void;
32
+ onSelect: (command: string) => void;
33
+ }
34
+
35
+ export function HelpMenu({ onClose, onSelect }: HelpMenuProps) {
36
+ const [selectedIndex, setSelectedIndex] = useState(0);
37
+ const [selectedCategory, setSelectedCategory] = useState<'all' | MenuItem['category']>('all');
38
+
39
+ const filteredItems = selectedCategory === 'all'
40
+ ? MENU_ITEMS
41
+ : MENU_ITEMS.filter(item => item.category === selectedCategory);
42
+
43
+ useInput((input, key) => {
44
+ if (key.escape) {
45
+ onClose();
46
+ return;
47
+ }
48
+
49
+ if (key.upArrow) {
50
+ setSelectedIndex(prev => (prev > 0 ? prev - 1 : filteredItems.length - 1));
51
+ }
52
+
53
+ if (key.downArrow) {
54
+ setSelectedIndex(prev => (prev < filteredItems.length - 1 ? prev + 1 : 0));
55
+ }
56
+
57
+ if (key.leftArrow || key.rightArrow) {
58
+ const categories: Array<'all' | MenuItem['category']> = ['all', 'navigation', 'actions', 'settings'];
59
+ const currentIdx = categories.indexOf(selectedCategory);
60
+ if (key.leftArrow) {
61
+ setSelectedCategory(categories[currentIdx > 0 ? currentIdx - 1 : categories.length - 1]!);
62
+ } else {
63
+ setSelectedCategory(categories[currentIdx < categories.length - 1 ? currentIdx + 1 : 0]!);
64
+ }
65
+ setSelectedIndex(0);
66
+ }
67
+
68
+ if (key.return) {
69
+ const item = filteredItems[selectedIndex];
70
+ if (item) {
71
+ onSelect(item.command);
72
+ onClose();
73
+ }
74
+ }
75
+ });
76
+
77
+ const categories: Array<{ key: 'all' | MenuItem['category']; label: string }> = [
78
+ { key: 'all', label: 'All' },
79
+ { key: 'navigation', label: 'Navigation' },
80
+ { key: 'actions', label: 'Actions' },
81
+ { key: 'settings', label: 'Settings' },
82
+ ];
83
+
84
+ return (
85
+ <Box
86
+ flexDirection="column"
87
+ borderStyle="round"
88
+ borderColor="cyan"
89
+ padding={1}
90
+ width={60}
91
+ >
92
+ {/* Header */}
93
+ <Box justifyContent="center" marginBottom={1}>
94
+ <Text bold color="cyan">C-napse Help</Text>
95
+ </Box>
96
+
97
+ {/* Category Tabs */}
98
+ <Box justifyContent="center" marginBottom={1}>
99
+ {categories.map((cat, idx) => (
100
+ <React.Fragment key={cat.key}>
101
+ <Text
102
+ color={selectedCategory === cat.key ? 'cyan' : 'gray'}
103
+ bold={selectedCategory === cat.key}
104
+ >
105
+ {cat.label}
106
+ </Text>
107
+ {idx < categories.length - 1 && <Text color="gray"> │ </Text>}
108
+ </React.Fragment>
109
+ ))}
110
+ </Box>
111
+
112
+ <Box marginBottom={1}>
113
+ <Text color="gray" dimColor>Use ←→ to switch tabs, ↑↓ to navigate, Enter to select</Text>
114
+ </Box>
115
+
116
+ {/* Menu Items */}
117
+ <Box flexDirection="column">
118
+ {filteredItems.map((item, index) => (
119
+ <Box key={item.command}>
120
+ <Text color={index === selectedIndex ? 'cyan' : 'white'}>
121
+ {index === selectedIndex ? '❯ ' : ' '}
122
+ </Text>
123
+ <Box width={12}>
124
+ <Text bold={index === selectedIndex} color={index === selectedIndex ? 'cyan' : 'white'}>
125
+ {item.command}
126
+ </Text>
127
+ </Box>
128
+ {item.shortcut && (
129
+ <Box width={10}>
130
+ <Text color="yellow" dimColor>{item.shortcut}</Text>
131
+ </Box>
132
+ )}
133
+ <Text color="gray">{item.description}</Text>
134
+ </Box>
135
+ ))}
136
+ </Box>
137
+
138
+ {/* Footer */}
139
+ <Box marginTop={1} justifyContent="center">
140
+ <Text color="gray" dimColor>Press Esc to close</Text>
141
+ </Box>
142
+ </Box>
143
+ );
144
+ }