@projectservan8n/cnapse 0.4.0 → 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.
- package/dist/index.js +706 -268
- package/package.json +1 -1
- package/src/components/App.tsx +172 -291
- package/src/components/HelpMenu.tsx +2 -1
- package/src/components/ProviderSelector.tsx +176 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/useChat.ts +149 -0
- package/src/hooks/useTasks.ts +63 -0
- package/src/hooks/useTelegram.ts +91 -0
- package/src/hooks/useVision.ts +47 -0
- package/src/lib/tasks.ts +249 -34
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { getConfig, setProvider, setModel } from '../lib/config.js';
|
|
4
|
+
|
|
5
|
+
interface ProviderSelectorProps {
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onSelect: (provider: string, model: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ProviderOption {
|
|
11
|
+
id: 'ollama' | 'openrouter' | 'anthropic' | 'openai';
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
defaultModel: string;
|
|
15
|
+
models: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PROVIDERS: ProviderOption[] = [
|
|
19
|
+
{
|
|
20
|
+
id: 'ollama',
|
|
21
|
+
name: 'Ollama',
|
|
22
|
+
description: 'Local AI - Free, private, no API key',
|
|
23
|
+
defaultModel: 'qwen2.5:0.5b',
|
|
24
|
+
models: ['qwen2.5:0.5b', 'qwen2.5:1.5b', 'qwen2.5:7b', 'llama3.2:1b', 'llama3.2:3b', 'mistral:7b', 'codellama:7b', 'llava:7b'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'openrouter',
|
|
28
|
+
name: 'OpenRouter',
|
|
29
|
+
description: 'Many models, pay-per-use',
|
|
30
|
+
defaultModel: 'qwen/qwen-2.5-coder-32b-instruct',
|
|
31
|
+
models: [
|
|
32
|
+
'qwen/qwen-2.5-coder-32b-instruct',
|
|
33
|
+
'anthropic/claude-3.5-sonnet',
|
|
34
|
+
'openai/gpt-4o',
|
|
35
|
+
'openai/gpt-4o-mini',
|
|
36
|
+
'google/gemini-pro-1.5',
|
|
37
|
+
'meta-llama/llama-3.1-70b-instruct',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'anthropic',
|
|
42
|
+
name: 'Anthropic',
|
|
43
|
+
description: 'Claude models - Best for coding',
|
|
44
|
+
defaultModel: 'claude-3-5-sonnet-20241022',
|
|
45
|
+
models: ['claude-3-5-sonnet-20241022', 'claude-3-opus-20240229', 'claude-3-haiku-20240307'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'openai',
|
|
49
|
+
name: 'OpenAI',
|
|
50
|
+
description: 'GPT models',
|
|
51
|
+
defaultModel: 'gpt-4o',
|
|
52
|
+
models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
type SelectionMode = 'provider' | 'model';
|
|
57
|
+
|
|
58
|
+
export function ProviderSelector({ onClose, onSelect }: ProviderSelectorProps) {
|
|
59
|
+
const config = getConfig();
|
|
60
|
+
const [mode, setMode] = useState<SelectionMode>('provider');
|
|
61
|
+
const [providerIndex, setProviderIndex] = useState(() => {
|
|
62
|
+
const idx = PROVIDERS.findIndex(p => p.id === config.provider);
|
|
63
|
+
return idx >= 0 ? idx : 0;
|
|
64
|
+
});
|
|
65
|
+
const [modelIndex, setModelIndex] = useState(0);
|
|
66
|
+
const [selectedProvider, setSelectedProvider] = useState<ProviderOption | null>(null);
|
|
67
|
+
|
|
68
|
+
useInput((input, key) => {
|
|
69
|
+
if (key.escape) {
|
|
70
|
+
onClose();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (mode === 'provider') {
|
|
75
|
+
if (key.upArrow) {
|
|
76
|
+
setProviderIndex(prev => (prev > 0 ? prev - 1 : PROVIDERS.length - 1));
|
|
77
|
+
} else if (key.downArrow) {
|
|
78
|
+
setProviderIndex(prev => (prev < PROVIDERS.length - 1 ? prev + 1 : 0));
|
|
79
|
+
} else if (key.return) {
|
|
80
|
+
const provider = PROVIDERS[providerIndex]!;
|
|
81
|
+
setSelectedProvider(provider);
|
|
82
|
+
// Find current model index if it exists in the provider's models
|
|
83
|
+
const currentModelIdx = provider.models.findIndex(m => m === config.model);
|
|
84
|
+
setModelIndex(currentModelIdx >= 0 ? currentModelIdx : 0);
|
|
85
|
+
setMode('model');
|
|
86
|
+
}
|
|
87
|
+
} else if (mode === 'model' && selectedProvider) {
|
|
88
|
+
if (key.upArrow) {
|
|
89
|
+
setModelIndex(prev => (prev > 0 ? prev - 1 : selectedProvider.models.length - 1));
|
|
90
|
+
} else if (key.downArrow) {
|
|
91
|
+
setModelIndex(prev => (prev < selectedProvider.models.length - 1 ? prev + 1 : 0));
|
|
92
|
+
} else if (key.return) {
|
|
93
|
+
const model = selectedProvider.models[modelIndex]!;
|
|
94
|
+
// Save to config
|
|
95
|
+
setProvider(selectedProvider.id);
|
|
96
|
+
setModel(model);
|
|
97
|
+
onSelect(selectedProvider.id, model);
|
|
98
|
+
onClose();
|
|
99
|
+
} else if (key.leftArrow || input === 'b') {
|
|
100
|
+
setMode('provider');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (mode === 'provider') {
|
|
106
|
+
return (
|
|
107
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
|
|
108
|
+
<Box marginBottom={1}>
|
|
109
|
+
<Text bold color="cyan">Select Provider</Text>
|
|
110
|
+
</Box>
|
|
111
|
+
<Box marginBottom={1}>
|
|
112
|
+
<Text color="gray">Use arrows to navigate, Enter to select, Esc to cancel</Text>
|
|
113
|
+
</Box>
|
|
114
|
+
|
|
115
|
+
{PROVIDERS.map((provider, index) => {
|
|
116
|
+
const isSelected = index === providerIndex;
|
|
117
|
+
const isCurrent = provider.id === config.provider;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Box key={provider.id} marginY={0}>
|
|
121
|
+
<Text color={isSelected ? 'cyan' : 'white'}>
|
|
122
|
+
{isSelected ? '❯ ' : ' '}
|
|
123
|
+
{provider.name}
|
|
124
|
+
{isCurrent && <Text color="green"> (current)</Text>}
|
|
125
|
+
</Text>
|
|
126
|
+
{isSelected && (
|
|
127
|
+
<Text color="gray"> - {provider.description}</Text>
|
|
128
|
+
)}
|
|
129
|
+
</Box>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
|
|
133
|
+
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
134
|
+
<Text color="gray">
|
|
135
|
+
Current: {config.provider} / {config.model}
|
|
136
|
+
</Text>
|
|
137
|
+
</Box>
|
|
138
|
+
</Box>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Model selection mode
|
|
143
|
+
return (
|
|
144
|
+
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
|
|
145
|
+
<Box marginBottom={1}>
|
|
146
|
+
<Text bold color="cyan">Select Model for {selectedProvider?.name}</Text>
|
|
147
|
+
</Box>
|
|
148
|
+
<Box marginBottom={1}>
|
|
149
|
+
<Text color="gray">Arrows to navigate, Enter to select, B/Left to go back</Text>
|
|
150
|
+
</Box>
|
|
151
|
+
|
|
152
|
+
{selectedProvider?.models.map((model, index) => {
|
|
153
|
+
const isSelected = index === modelIndex;
|
|
154
|
+
const isCurrent = model === config.model && selectedProvider.id === config.provider;
|
|
155
|
+
const isDefault = model === selectedProvider.defaultModel;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<Box key={model} marginY={0}>
|
|
159
|
+
<Text color={isSelected ? 'cyan' : 'white'}>
|
|
160
|
+
{isSelected ? '❯ ' : ' '}
|
|
161
|
+
{model}
|
|
162
|
+
{isCurrent && <Text color="green"> (current)</Text>}
|
|
163
|
+
{isDefault && !isCurrent && <Text color="yellow"> (default)</Text>}
|
|
164
|
+
</Text>
|
|
165
|
+
</Box>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
|
|
169
|
+
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
170
|
+
<Text color="gray">
|
|
171
|
+
Provider: {selectedProvider?.name}
|
|
172
|
+
</Text>
|
|
173
|
+
</Box>
|
|
174
|
+
</Box>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom hooks for C-napse
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { useChat } from './useChat.js';
|
|
6
|
+
export type { ChatMessage, UseChatResult } from './useChat.js';
|
|
7
|
+
|
|
8
|
+
export { useVision } from './useVision.js';
|
|
9
|
+
export type { UseVisionResult } from './useVision.js';
|
|
10
|
+
|
|
11
|
+
export { useTelegram } from './useTelegram.js';
|
|
12
|
+
export type { UseTelegramResult } from './useTelegram.js';
|
|
13
|
+
|
|
14
|
+
export { useTasks } from './useTasks.js';
|
|
15
|
+
export type { UseTasksResult } from './useTasks.js';
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Hook - AI conversation management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
6
|
+
import { chat, Message } from '../lib/api.js';
|
|
7
|
+
import { getScreenDescription } from '../lib/screen.js';
|
|
8
|
+
|
|
9
|
+
export interface ChatMessage {
|
|
10
|
+
id: string;
|
|
11
|
+
role: 'user' | 'assistant' | 'system';
|
|
12
|
+
content: string;
|
|
13
|
+
timestamp: Date;
|
|
14
|
+
isStreaming?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseChatResult {
|
|
18
|
+
messages: ChatMessage[];
|
|
19
|
+
isProcessing: boolean;
|
|
20
|
+
error: string | null;
|
|
21
|
+
sendMessage: (content: string) => Promise<void>;
|
|
22
|
+
addSystemMessage: (content: string) => void;
|
|
23
|
+
clearMessages: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const WELCOME_MESSAGE: ChatMessage = {
|
|
27
|
+
id: '0',
|
|
28
|
+
role: 'system',
|
|
29
|
+
content: 'Welcome to C-napse! Type your message and press Enter.\n\nShortcuts: Ctrl+H for help, Ctrl+P for provider',
|
|
30
|
+
timestamp: new Date(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function useChat(screenWatch: boolean = false): UseChatResult {
|
|
34
|
+
const [messages, setMessages] = useState<ChatMessage[]>([WELCOME_MESSAGE]);
|
|
35
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
const screenContextRef = useRef<string | null>(null);
|
|
38
|
+
|
|
39
|
+
// Screen watching effect
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!screenWatch) {
|
|
42
|
+
screenContextRef.current = null;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const checkScreen = async () => {
|
|
47
|
+
const desc = await getScreenDescription();
|
|
48
|
+
if (desc) {
|
|
49
|
+
screenContextRef.current = desc;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
checkScreen();
|
|
54
|
+
const interval = setInterval(checkScreen, 5000);
|
|
55
|
+
return () => clearInterval(interval);
|
|
56
|
+
}, [screenWatch]);
|
|
57
|
+
|
|
58
|
+
const addSystemMessage = useCallback((content: string) => {
|
|
59
|
+
setMessages(prev => [
|
|
60
|
+
...prev,
|
|
61
|
+
{
|
|
62
|
+
id: Date.now().toString(),
|
|
63
|
+
role: 'system',
|
|
64
|
+
content,
|
|
65
|
+
timestamp: new Date(),
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
}, []);
|
|
69
|
+
|
|
70
|
+
const sendMessage = useCallback(async (content: string) => {
|
|
71
|
+
if (!content.trim() || isProcessing) return;
|
|
72
|
+
|
|
73
|
+
setError(null);
|
|
74
|
+
|
|
75
|
+
// Add user message
|
|
76
|
+
const userMsg: ChatMessage = {
|
|
77
|
+
id: Date.now().toString(),
|
|
78
|
+
role: 'user',
|
|
79
|
+
content,
|
|
80
|
+
timestamp: new Date(),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Add assistant placeholder
|
|
84
|
+
const assistantId = (Date.now() + 1).toString();
|
|
85
|
+
const assistantMsg: ChatMessage = {
|
|
86
|
+
id: assistantId,
|
|
87
|
+
role: 'assistant',
|
|
88
|
+
content: '',
|
|
89
|
+
timestamp: new Date(),
|
|
90
|
+
isStreaming: true,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
setMessages(prev => [...prev, userMsg, assistantMsg]);
|
|
94
|
+
setIsProcessing(true);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Build message history
|
|
98
|
+
const apiMessages: Message[] = messages
|
|
99
|
+
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
100
|
+
.slice(-10)
|
|
101
|
+
.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }));
|
|
102
|
+
|
|
103
|
+
// Add screen context if watching
|
|
104
|
+
let finalContent = content;
|
|
105
|
+
if (screenWatch && screenContextRef.current) {
|
|
106
|
+
finalContent = `[Screen context: ${screenContextRef.current}]\n\n${content}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
apiMessages.push({ role: 'user', content: finalContent });
|
|
110
|
+
|
|
111
|
+
const response = await chat(apiMessages);
|
|
112
|
+
|
|
113
|
+
// Update assistant message
|
|
114
|
+
setMessages(prev =>
|
|
115
|
+
prev.map(m =>
|
|
116
|
+
m.id === assistantId
|
|
117
|
+
? { ...m, content: response.content || '(no response)', isStreaming: false }
|
|
118
|
+
: m
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
123
|
+
setError(errorMsg);
|
|
124
|
+
setMessages(prev =>
|
|
125
|
+
prev.map(m =>
|
|
126
|
+
m.id === assistantId
|
|
127
|
+
? { ...m, content: `Error: ${errorMsg}`, isStreaming: false }
|
|
128
|
+
: m
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
} finally {
|
|
132
|
+
setIsProcessing(false);
|
|
133
|
+
}
|
|
134
|
+
}, [messages, isProcessing, screenWatch]);
|
|
135
|
+
|
|
136
|
+
const clearMessages = useCallback(() => {
|
|
137
|
+
setMessages([WELCOME_MESSAGE]);
|
|
138
|
+
setError(null);
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
messages,
|
|
143
|
+
isProcessing,
|
|
144
|
+
error,
|
|
145
|
+
sendMessage,
|
|
146
|
+
addSystemMessage,
|
|
147
|
+
clearMessages,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks Hook - Multi-step task automation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useCallback } from 'react';
|
|
6
|
+
import { parseTask, executeTask, formatTask, getTaskMemoryStats, clearTaskMemory, Task, TaskStep } from '../lib/tasks.js';
|
|
7
|
+
|
|
8
|
+
export interface UseTasksResult {
|
|
9
|
+
isRunning: boolean;
|
|
10
|
+
currentTask: Task | null;
|
|
11
|
+
currentStep: TaskStep | null;
|
|
12
|
+
error: string | null;
|
|
13
|
+
run: (description: string) => Promise<Task>;
|
|
14
|
+
format: (task: Task) => string;
|
|
15
|
+
getMemoryStats: () => { patternCount: number; totalUses: number; topPatterns: string[] };
|
|
16
|
+
clearMemory: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useTasks(onProgress?: (task: Task, step: TaskStep) => void): UseTasksResult {
|
|
20
|
+
const [isRunning, setIsRunning] = useState(false);
|
|
21
|
+
const [currentTask, setCurrentTask] = useState<Task | null>(null);
|
|
22
|
+
const [currentStep, setCurrentStep] = useState<TaskStep | null>(null);
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
const run = useCallback(async (description: string): Promise<Task> => {
|
|
26
|
+
setIsRunning(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Parse the task
|
|
31
|
+
const task = await parseTask(description);
|
|
32
|
+
setCurrentTask(task);
|
|
33
|
+
|
|
34
|
+
// Execute with progress callback
|
|
35
|
+
const result = await executeTask(task, (updatedTask, step) => {
|
|
36
|
+
setCurrentTask({ ...updatedTask });
|
|
37
|
+
setCurrentStep(step);
|
|
38
|
+
onProgress?.(updatedTask, step);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
setCurrentTask(result);
|
|
42
|
+
return result;
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const errorMsg = err instanceof Error ? err.message : 'Task failed';
|
|
45
|
+
setError(errorMsg);
|
|
46
|
+
throw err;
|
|
47
|
+
} finally {
|
|
48
|
+
setIsRunning(false);
|
|
49
|
+
setCurrentStep(null);
|
|
50
|
+
}
|
|
51
|
+
}, [onProgress]);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
isRunning,
|
|
55
|
+
currentTask,
|
|
56
|
+
currentStep,
|
|
57
|
+
error,
|
|
58
|
+
run,
|
|
59
|
+
format: formatTask,
|
|
60
|
+
getMemoryStats: getTaskMemoryStats,
|
|
61
|
+
clearMemory: clearTaskMemory,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram Hook - Remote PC control via Telegram bot
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
6
|
+
import { getTelegramBot, TelegramMessage } from '../services/telegram.js';
|
|
7
|
+
|
|
8
|
+
export interface UseTelegramResult {
|
|
9
|
+
isEnabled: boolean;
|
|
10
|
+
isStarting: boolean;
|
|
11
|
+
error: string | null;
|
|
12
|
+
lastMessage: TelegramMessage | null;
|
|
13
|
+
toggle: () => Promise<void>;
|
|
14
|
+
start: () => Promise<void>;
|
|
15
|
+
stop: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useTelegram(onMessage?: (msg: TelegramMessage) => void): UseTelegramResult {
|
|
19
|
+
const [isEnabled, setIsEnabled] = useState(false);
|
|
20
|
+
const [isStarting, setIsStarting] = useState(false);
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
const [lastMessage, setLastMessage] = useState<TelegramMessage | null>(null);
|
|
23
|
+
const onMessageRef = useRef(onMessage);
|
|
24
|
+
|
|
25
|
+
// Keep callback ref updated
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
onMessageRef.current = onMessage;
|
|
28
|
+
}, [onMessage]);
|
|
29
|
+
|
|
30
|
+
const start = useCallback(async () => {
|
|
31
|
+
if (isEnabled) return;
|
|
32
|
+
|
|
33
|
+
setIsStarting(true);
|
|
34
|
+
setError(null);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const bot = getTelegramBot();
|
|
38
|
+
|
|
39
|
+
// Setup event handlers
|
|
40
|
+
bot.on('message', (msg: TelegramMessage) => {
|
|
41
|
+
setLastMessage(msg);
|
|
42
|
+
onMessageRef.current?.(msg);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
bot.on('error', (err: Error) => {
|
|
46
|
+
setError(err.message);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await bot.start();
|
|
50
|
+
setIsEnabled(true);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const errorMsg = err instanceof Error ? err.message : 'Failed to start Telegram bot';
|
|
53
|
+
setError(errorMsg);
|
|
54
|
+
throw err;
|
|
55
|
+
} finally {
|
|
56
|
+
setIsStarting(false);
|
|
57
|
+
}
|
|
58
|
+
}, [isEnabled]);
|
|
59
|
+
|
|
60
|
+
const stop = useCallback(async () => {
|
|
61
|
+
if (!isEnabled) return;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const bot = getTelegramBot();
|
|
65
|
+
await bot.stop();
|
|
66
|
+
setIsEnabled(false);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
const errorMsg = err instanceof Error ? err.message : 'Failed to stop Telegram bot';
|
|
69
|
+
setError(errorMsg);
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}, [isEnabled]);
|
|
73
|
+
|
|
74
|
+
const toggle = useCallback(async () => {
|
|
75
|
+
if (isEnabled) {
|
|
76
|
+
await stop();
|
|
77
|
+
} else {
|
|
78
|
+
await start();
|
|
79
|
+
}
|
|
80
|
+
}, [isEnabled, start, stop]);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
isEnabled,
|
|
84
|
+
isStarting,
|
|
85
|
+
error,
|
|
86
|
+
lastMessage,
|
|
87
|
+
toggle,
|
|
88
|
+
start,
|
|
89
|
+
stop,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vision Hook - Screenshot capture and AI description
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useCallback } from 'react';
|
|
6
|
+
import { describeScreen } from '../lib/vision.js';
|
|
7
|
+
|
|
8
|
+
export interface UseVisionResult {
|
|
9
|
+
isAnalyzing: boolean;
|
|
10
|
+
lastDescription: string | null;
|
|
11
|
+
lastScreenshot: string | null;
|
|
12
|
+
error: string | null;
|
|
13
|
+
analyze: () => Promise<string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useVision(): UseVisionResult {
|
|
17
|
+
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
|
18
|
+
const [lastDescription, setLastDescription] = useState<string | null>(null);
|
|
19
|
+
const [lastScreenshot, setLastScreenshot] = useState<string | null>(null);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
const analyze = useCallback(async (): Promise<string> => {
|
|
23
|
+
setIsAnalyzing(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = await describeScreen();
|
|
28
|
+
setLastDescription(result.description);
|
|
29
|
+
setLastScreenshot(result.screenshot);
|
|
30
|
+
return result.description;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const errorMsg = err instanceof Error ? err.message : 'Vision analysis failed';
|
|
33
|
+
setError(errorMsg);
|
|
34
|
+
throw err;
|
|
35
|
+
} finally {
|
|
36
|
+
setIsAnalyzing(false);
|
|
37
|
+
}
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
isAnalyzing,
|
|
42
|
+
lastDescription,
|
|
43
|
+
lastScreenshot,
|
|
44
|
+
error,
|
|
45
|
+
analyze,
|
|
46
|
+
};
|
|
47
|
+
}
|