agent-hustle-demo 1.0.1
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/README.md +429 -0
- package/dist/HustleChat-BC9wvWVA.d.ts +90 -0
- package/dist/HustleChat-BcrKkkyn.d.cts +90 -0
- package/dist/browser/hustle-react.js +14854 -0
- package/dist/browser/hustle-react.js.map +1 -0
- package/dist/components/index.cjs +3141 -0
- package/dist/components/index.cjs.map +1 -0
- package/dist/components/index.d.cts +20 -0
- package/dist/components/index.d.ts +20 -0
- package/dist/components/index.js +3112 -0
- package/dist/components/index.js.map +1 -0
- package/dist/hooks/index.cjs +845 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.cts +6 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +838 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hustle-Kj0X8qXC.d.cts +193 -0
- package/dist/hustle-Kj0X8qXC.d.ts +193 -0
- package/dist/index-ChUsRBwL.d.ts +152 -0
- package/dist/index-DE1N7C3W.d.cts +152 -0
- package/dist/index-DuPFrMZy.d.cts +214 -0
- package/dist/index-kFIdHjNw.d.ts +214 -0
- package/dist/index.cjs +3746 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +271 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +3697 -0
- package/dist/index.js.map +1 -0
- package/dist/providers/index.cjs +844 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +5 -0
- package/dist/providers/index.d.ts +5 -0
- package/dist/providers/index.js +838 -0
- package/dist/providers/index.js.map +1 -0
- package/package.json +80 -0
- package/src/components/AuthStatus.tsx +352 -0
- package/src/components/ConnectButton.tsx +421 -0
- package/src/components/HustleChat.tsx +1273 -0
- package/src/components/MarkdownContent.tsx +431 -0
- package/src/components/index.ts +15 -0
- package/src/hooks/index.ts +40 -0
- package/src/hooks/useEmblemAuth.ts +27 -0
- package/src/hooks/useHustle.ts +36 -0
- package/src/hooks/usePlugins.ts +135 -0
- package/src/index.ts +142 -0
- package/src/plugins/index.ts +48 -0
- package/src/plugins/migrateFun.ts +211 -0
- package/src/plugins/predictionMarket.ts +411 -0
- package/src/providers/EmblemAuthProvider.tsx +319 -0
- package/src/providers/HustleProvider.tsx +540 -0
- package/src/providers/index.ts +6 -0
- package/src/styles/index.ts +2 -0
- package/src/styles/tokens.ts +447 -0
- package/src/types/auth.ts +85 -0
- package/src/types/hustle.ts +217 -0
- package/src/types/index.ts +49 -0
- package/src/types/plugin.ts +180 -0
- package/src/utils/index.ts +122 -0
- package/src/utils/pluginRegistry.ts +375 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useEffect,
|
|
8
|
+
useCallback,
|
|
9
|
+
useMemo,
|
|
10
|
+
useRef,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { HustleIncognitoClient } from 'hustle-incognito';
|
|
13
|
+
import { useEmblemAuth } from './EmblemAuthProvider';
|
|
14
|
+
import { usePlugins } from '../hooks/usePlugins';
|
|
15
|
+
import type {
|
|
16
|
+
Model,
|
|
17
|
+
ChatOptions,
|
|
18
|
+
StreamOptions,
|
|
19
|
+
StreamChunk,
|
|
20
|
+
ChatResponse,
|
|
21
|
+
Attachment,
|
|
22
|
+
HustleContextValue,
|
|
23
|
+
HustleProviderProps,
|
|
24
|
+
ChatMessage,
|
|
25
|
+
HydratedPlugin,
|
|
26
|
+
} from '../types';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hustle context - undefined when not within provider
|
|
30
|
+
*/
|
|
31
|
+
const HustleContext = createContext<HustleContextValue | undefined>(undefined);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Default Hustle API URL
|
|
35
|
+
*/
|
|
36
|
+
const DEFAULT_HUSTLE_API_URL = 'https://agenthustle.ai';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Module-level counter for auto-generating instance IDs.
|
|
40
|
+
* Resets on page load, but render order is deterministic.
|
|
41
|
+
*/
|
|
42
|
+
let instanceCounter = 0;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Track mounted instances without explicit IDs for dev warnings
|
|
46
|
+
*/
|
|
47
|
+
const mountedAutoInstances = new Set<string>();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* HustleProvider - Provides Hustle SDK functionality to the app
|
|
51
|
+
*
|
|
52
|
+
* IMPORTANT: This provider depends on EmblemAuthProvider and must be nested within it.
|
|
53
|
+
* It uses the modern pattern of passing the auth SDK instance to HustleIncognitoClient,
|
|
54
|
+
* NOT the deprecated api-key pattern.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* <EmblemAuthProvider appId="your-app-id">
|
|
59
|
+
* <HustleProvider hustleApiUrl="https://dev.agenthustle.ai">
|
|
60
|
+
* <App />
|
|
61
|
+
* </HustleProvider>
|
|
62
|
+
* </EmblemAuthProvider>
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export function HustleProvider({
|
|
66
|
+
children,
|
|
67
|
+
hustleApiUrl = DEFAULT_HUSTLE_API_URL,
|
|
68
|
+
debug = false,
|
|
69
|
+
instanceId: explicitInstanceId,
|
|
70
|
+
}: HustleProviderProps) {
|
|
71
|
+
// Generate stable instance ID - explicit or auto-generated based on mount order
|
|
72
|
+
const [resolvedInstanceId] = useState(() => {
|
|
73
|
+
if (explicitInstanceId) {
|
|
74
|
+
return explicitInstanceId;
|
|
75
|
+
}
|
|
76
|
+
// Auto-generate based on mount order
|
|
77
|
+
const autoId = `instance-${++instanceCounter}`;
|
|
78
|
+
mountedAutoInstances.add(autoId);
|
|
79
|
+
return autoId;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Track if this is an auto-generated instance for cleanup
|
|
83
|
+
const isAutoInstance = !explicitInstanceId;
|
|
84
|
+
|
|
85
|
+
// Dev warning for multiple auto-generated instances
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (isAutoInstance && mountedAutoInstances.size > 1 && process.env.NODE_ENV !== 'production') {
|
|
88
|
+
console.warn(
|
|
89
|
+
`[Hustle] Multiple HustleProviders detected without explicit instanceId. ` +
|
|
90
|
+
`For stable settings persistence, consider adding instanceId prop:\n` +
|
|
91
|
+
` <HustleProvider instanceId="my-chat-name">`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Cleanup on unmount
|
|
96
|
+
return () => {
|
|
97
|
+
if (isAutoInstance) {
|
|
98
|
+
mountedAutoInstances.delete(resolvedInstanceId);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}, [isAutoInstance, resolvedInstanceId]);
|
|
102
|
+
|
|
103
|
+
// Get auth context - this provider REQUIRES EmblemAuthProvider
|
|
104
|
+
const { authSDK, isAuthenticated } = useEmblemAuth();
|
|
105
|
+
|
|
106
|
+
// Get plugins with instance scoping
|
|
107
|
+
const { enabledPlugins } = usePlugins(resolvedInstanceId);
|
|
108
|
+
|
|
109
|
+
// State
|
|
110
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
111
|
+
const [error, setError] = useState<Error | null>(null);
|
|
112
|
+
const [models, setModels] = useState<Model[]>([]);
|
|
113
|
+
|
|
114
|
+
// Track registered plugins to avoid re-registering
|
|
115
|
+
const registeredPluginsRef = useRef<Set<string>>(new Set());
|
|
116
|
+
|
|
117
|
+
// Settings storage key - scoped to instance
|
|
118
|
+
const SETTINGS_KEY = `hustle-settings-${resolvedInstanceId}`;
|
|
119
|
+
|
|
120
|
+
// Load initial settings from localStorage
|
|
121
|
+
const loadSettings = () => {
|
|
122
|
+
if (typeof window === 'undefined') return { selectedModel: '', systemPrompt: '', skipServerPrompt: false };
|
|
123
|
+
try {
|
|
124
|
+
const stored = localStorage.getItem(SETTINGS_KEY);
|
|
125
|
+
if (stored) {
|
|
126
|
+
return JSON.parse(stored);
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// Ignore parse errors
|
|
130
|
+
}
|
|
131
|
+
return { selectedModel: '', systemPrompt: '', skipServerPrompt: false };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const initialSettings = loadSettings();
|
|
135
|
+
|
|
136
|
+
// Settings state (initialized from localStorage)
|
|
137
|
+
const [selectedModel, setSelectedModelState] = useState<string>(initialSettings.selectedModel);
|
|
138
|
+
const [systemPrompt, setSystemPromptState] = useState<string>(initialSettings.systemPrompt);
|
|
139
|
+
const [skipServerPrompt, setSkipServerPromptState] = useState<boolean>(initialSettings.skipServerPrompt);
|
|
140
|
+
|
|
141
|
+
// Persist settings to localStorage
|
|
142
|
+
const saveSettings = useCallback((settings: { selectedModel: string; systemPrompt: string; skipServerPrompt: boolean }) => {
|
|
143
|
+
if (typeof window === 'undefined') return;
|
|
144
|
+
try {
|
|
145
|
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
|
|
146
|
+
} catch {
|
|
147
|
+
// Ignore storage errors
|
|
148
|
+
}
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
151
|
+
// Wrapped setters that also persist
|
|
152
|
+
const setSelectedModel = useCallback((value: string) => {
|
|
153
|
+
setSelectedModelState(value);
|
|
154
|
+
saveSettings({ selectedModel: value, systemPrompt, skipServerPrompt });
|
|
155
|
+
}, [systemPrompt, skipServerPrompt, saveSettings]);
|
|
156
|
+
|
|
157
|
+
const setSystemPrompt = useCallback((value: string) => {
|
|
158
|
+
setSystemPromptState(value);
|
|
159
|
+
saveSettings({ selectedModel, systemPrompt: value, skipServerPrompt });
|
|
160
|
+
}, [selectedModel, skipServerPrompt, saveSettings]);
|
|
161
|
+
|
|
162
|
+
const setSkipServerPrompt = useCallback((value: boolean) => {
|
|
163
|
+
setSkipServerPromptState(value);
|
|
164
|
+
saveSettings({ selectedModel, systemPrompt, skipServerPrompt: value });
|
|
165
|
+
}, [selectedModel, systemPrompt, saveSettings]);
|
|
166
|
+
|
|
167
|
+
// Debug logger
|
|
168
|
+
const log = useCallback(
|
|
169
|
+
(message: string, ...args: unknown[]) => {
|
|
170
|
+
if (debug) {
|
|
171
|
+
console.log(`[Hustle] ${message}`, ...args);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
[debug]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Create the Hustle client with the auth SDK
|
|
179
|
+
* This is the CORRECT pattern - using sdk: authSDK, NOT apiKey
|
|
180
|
+
*/
|
|
181
|
+
const client = useMemo(() => {
|
|
182
|
+
if (!authSDK || !isAuthenticated) {
|
|
183
|
+
log('Client not created - auth not ready');
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
log('Creating HustleIncognitoClient with auth SDK');
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const hustleClient = new HustleIncognitoClient({
|
|
191
|
+
sdk: authSDK, // CORRECT: Pass auth SDK instance, NOT apiKey
|
|
192
|
+
hustleApiUrl,
|
|
193
|
+
debug,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Subscribe to events (use any for SDK event types)
|
|
197
|
+
hustleClient.on('tool_start', (event: unknown) => {
|
|
198
|
+
log('Tool start:', event);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
hustleClient.on('tool_end', (event: unknown) => {
|
|
202
|
+
log('Tool end:', event);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
hustleClient.on('stream_end', (event: unknown) => {
|
|
206
|
+
log('Stream end:', event);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
return hustleClient;
|
|
210
|
+
} catch (err) {
|
|
211
|
+
log('Failed to create client:', err);
|
|
212
|
+
setError(err instanceof Error ? err : new Error('Failed to create Hustle client'));
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}, [authSDK, isAuthenticated, hustleApiUrl, debug, log]);
|
|
216
|
+
|
|
217
|
+
// Is ready when client exists
|
|
218
|
+
const isReady = client !== null;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Register enabled plugins with the client
|
|
222
|
+
*/
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!client) return;
|
|
225
|
+
|
|
226
|
+
const registerPlugins = async () => {
|
|
227
|
+
// Get the set of enabled plugin names
|
|
228
|
+
const enabledNames = new Set(enabledPlugins.map(p => p.name));
|
|
229
|
+
|
|
230
|
+
// Unregister plugins that were disabled
|
|
231
|
+
for (const name of registeredPluginsRef.current) {
|
|
232
|
+
if (!enabledNames.has(name)) {
|
|
233
|
+
log('Unregistering plugin:', name);
|
|
234
|
+
try {
|
|
235
|
+
await client.unuse(name);
|
|
236
|
+
registeredPluginsRef.current.delete(name);
|
|
237
|
+
log('Plugin unregistered:', name);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
log('Failed to unregister plugin:', name, err);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Register new plugins
|
|
245
|
+
for (const plugin of enabledPlugins) {
|
|
246
|
+
if (!registeredPluginsRef.current.has(plugin.name)) {
|
|
247
|
+
log('Registering plugin:', plugin.name);
|
|
248
|
+
try {
|
|
249
|
+
// The SDK's use() method registers the plugin
|
|
250
|
+
if (plugin.executors || plugin.hooks) {
|
|
251
|
+
// Cast to SDK's expected type (our types are compatible but TS is strict)
|
|
252
|
+
await client.use({
|
|
253
|
+
name: plugin.name,
|
|
254
|
+
version: plugin.version,
|
|
255
|
+
tools: plugin.tools,
|
|
256
|
+
executors: plugin.executors,
|
|
257
|
+
hooks: plugin.hooks,
|
|
258
|
+
} as Parameters<typeof client.use>[0]);
|
|
259
|
+
registeredPluginsRef.current.add(plugin.name);
|
|
260
|
+
log('Plugin registered:', plugin.name);
|
|
261
|
+
} else {
|
|
262
|
+
log('Plugin has no executors/hooks, skipping registration:', plugin.name);
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
log('Failed to register plugin:', plugin.name, err);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
registerPlugins();
|
|
272
|
+
}, [client, enabledPlugins, log]);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Load available models
|
|
276
|
+
*/
|
|
277
|
+
const loadModels = useCallback(async (): Promise<Model[]> => {
|
|
278
|
+
if (!client) {
|
|
279
|
+
log('Cannot load models - client not ready');
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
log('Loading models');
|
|
284
|
+
setIsLoading(true);
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const modelList = await client.getModels();
|
|
288
|
+
setModels(modelList as Model[]);
|
|
289
|
+
log('Loaded models:', modelList.length);
|
|
290
|
+
return modelList as Model[];
|
|
291
|
+
} catch (err) {
|
|
292
|
+
log('Failed to load models:', err);
|
|
293
|
+
setError(err instanceof Error ? err : new Error('Failed to load models'));
|
|
294
|
+
return [];
|
|
295
|
+
} finally {
|
|
296
|
+
setIsLoading(false);
|
|
297
|
+
}
|
|
298
|
+
}, [client, log]);
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Load models when client becomes ready
|
|
302
|
+
*/
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
if (client) {
|
|
305
|
+
loadModels();
|
|
306
|
+
}
|
|
307
|
+
}, [client, loadModels]);
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Send a chat message (non-streaming)
|
|
311
|
+
*/
|
|
312
|
+
const chat = useCallback(
|
|
313
|
+
async (options: ChatOptions): Promise<ChatResponse> => {
|
|
314
|
+
if (!client) {
|
|
315
|
+
throw new Error('Hustle client not ready. Please authenticate first.');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
log('Chat request:', options.messages.length, 'messages');
|
|
319
|
+
setIsLoading(true);
|
|
320
|
+
setError(null);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
// Build the messages array, prepending system prompt if provided
|
|
324
|
+
const effectiveSystemPrompt = options.systemPrompt || systemPrompt;
|
|
325
|
+
const messagesWithSystem: ChatMessage[] = [];
|
|
326
|
+
|
|
327
|
+
if (effectiveSystemPrompt) {
|
|
328
|
+
messagesWithSystem.push({ role: 'system', content: effectiveSystemPrompt });
|
|
329
|
+
}
|
|
330
|
+
messagesWithSystem.push(...options.messages);
|
|
331
|
+
|
|
332
|
+
// Build the options object for the SDK
|
|
333
|
+
const sdkOptions: Record<string, unknown> = {
|
|
334
|
+
messages: messagesWithSystem,
|
|
335
|
+
processChunks: true,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
if (options.model || selectedModel) {
|
|
339
|
+
sdkOptions.model = options.model || selectedModel;
|
|
340
|
+
}
|
|
341
|
+
if (options.overrideSystemPrompt ?? skipServerPrompt) {
|
|
342
|
+
sdkOptions.overrideSystemPrompt = true;
|
|
343
|
+
}
|
|
344
|
+
if (options.attachments) {
|
|
345
|
+
sdkOptions.attachments = options.attachments;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Call the SDK - it accepts an options object
|
|
349
|
+
const response = await (client as unknown as { chat: (opts: Record<string, unknown>) => Promise<unknown> }).chat(sdkOptions);
|
|
350
|
+
log('Chat response received');
|
|
351
|
+
return response as ChatResponse;
|
|
352
|
+
} catch (err) {
|
|
353
|
+
log('Chat error:', err);
|
|
354
|
+
const error = err instanceof Error ? err : new Error('Chat request failed');
|
|
355
|
+
setError(error);
|
|
356
|
+
throw error;
|
|
357
|
+
} finally {
|
|
358
|
+
setIsLoading(false);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
[client, selectedModel, systemPrompt, skipServerPrompt, log]
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Send a chat message with streaming response
|
|
366
|
+
*/
|
|
367
|
+
const chatStreamImpl = useCallback(
|
|
368
|
+
(options: StreamOptions): AsyncIterable<StreamChunk> => {
|
|
369
|
+
if (!client) {
|
|
370
|
+
// Return an async iterable that yields an error
|
|
371
|
+
return {
|
|
372
|
+
[Symbol.asyncIterator]: async function* () {
|
|
373
|
+
yield { type: 'error', value: { message: 'Hustle client not ready. Please authenticate first.' } } as StreamChunk;
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
log('Chat stream request:', options.messages.length, 'messages');
|
|
379
|
+
setError(null);
|
|
380
|
+
|
|
381
|
+
// Build the messages array, prepending system prompt if provided
|
|
382
|
+
const effectiveSystemPrompt = options.systemPrompt || systemPrompt;
|
|
383
|
+
const messagesWithSystem: ChatMessage[] = [];
|
|
384
|
+
|
|
385
|
+
if (effectiveSystemPrompt) {
|
|
386
|
+
messagesWithSystem.push({ role: 'system', content: effectiveSystemPrompt });
|
|
387
|
+
}
|
|
388
|
+
messagesWithSystem.push(...options.messages);
|
|
389
|
+
|
|
390
|
+
// Build the options object for the SDK
|
|
391
|
+
const sdkOptions: Record<string, unknown> = {
|
|
392
|
+
messages: messagesWithSystem,
|
|
393
|
+
processChunks: options.processChunks ?? true,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
if (options.model || selectedModel) {
|
|
397
|
+
sdkOptions.model = options.model || selectedModel;
|
|
398
|
+
}
|
|
399
|
+
if (options.overrideSystemPrompt ?? skipServerPrompt) {
|
|
400
|
+
sdkOptions.overrideSystemPrompt = true;
|
|
401
|
+
}
|
|
402
|
+
if (options.attachments) {
|
|
403
|
+
sdkOptions.attachments = options.attachments;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Get the stream from the client (cast through unknown for SDK compatibility)
|
|
407
|
+
const stream = client.chatStream(sdkOptions as unknown as Parameters<typeof client.chatStream>[0]);
|
|
408
|
+
|
|
409
|
+
// Wrap to add logging and type conversion
|
|
410
|
+
return {
|
|
411
|
+
[Symbol.asyncIterator]: async function* () {
|
|
412
|
+
try {
|
|
413
|
+
for await (const chunk of stream) {
|
|
414
|
+
// Type guard for chunk with type property
|
|
415
|
+
const typedChunk = chunk as { type?: string; value?: unknown };
|
|
416
|
+
|
|
417
|
+
if (typedChunk.type === 'text') {
|
|
418
|
+
const textValue = typedChunk.value as string;
|
|
419
|
+
log('Stream text chunk:', textValue?.substring(0, 50));
|
|
420
|
+
yield { type: 'text', value: textValue } as StreamChunk;
|
|
421
|
+
} else if (typedChunk.type === 'tool_call') {
|
|
422
|
+
log('Stream tool call:', typedChunk.value);
|
|
423
|
+
yield { type: 'tool_call', value: typedChunk.value } as StreamChunk;
|
|
424
|
+
} else if (typedChunk.type === 'tool_result') {
|
|
425
|
+
log('Stream tool result');
|
|
426
|
+
yield { type: 'tool_result', value: typedChunk.value } as StreamChunk;
|
|
427
|
+
} else if (typedChunk.type === 'error') {
|
|
428
|
+
const errorValue = typedChunk.value as { message?: string };
|
|
429
|
+
log('Stream error:', errorValue);
|
|
430
|
+
setError(new Error(errorValue?.message || 'Stream error'));
|
|
431
|
+
yield { type: 'error', value: { message: errorValue?.message || 'Stream error' } } as StreamChunk;
|
|
432
|
+
} else {
|
|
433
|
+
// Pass through unknown chunk types
|
|
434
|
+
yield chunk as StreamChunk;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch (err) {
|
|
438
|
+
log('Stream error:', err);
|
|
439
|
+
const error = err instanceof Error ? err : new Error('Stream failed');
|
|
440
|
+
setError(error);
|
|
441
|
+
yield { type: 'error', value: { message: error.message } } as StreamChunk;
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
},
|
|
446
|
+
[client, selectedModel, systemPrompt, skipServerPrompt, log]
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Upload a file
|
|
451
|
+
*/
|
|
452
|
+
const uploadFile = useCallback(
|
|
453
|
+
async (file: File): Promise<Attachment> => {
|
|
454
|
+
if (!client) {
|
|
455
|
+
throw new Error('Hustle client not ready. Please authenticate first.');
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
log('Uploading file:', file.name);
|
|
459
|
+
setIsLoading(true);
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const attachment = await client.uploadFile(file);
|
|
463
|
+
log('File uploaded:', attachment);
|
|
464
|
+
return attachment as Attachment;
|
|
465
|
+
} catch (err) {
|
|
466
|
+
log('Upload error:', err);
|
|
467
|
+
const error = err instanceof Error ? err : new Error('File upload failed');
|
|
468
|
+
setError(error);
|
|
469
|
+
throw error;
|
|
470
|
+
} finally {
|
|
471
|
+
setIsLoading(false);
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
[client, log]
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// Context value
|
|
478
|
+
const value: HustleContextValue = {
|
|
479
|
+
// Instance ID for scoped storage
|
|
480
|
+
instanceId: resolvedInstanceId,
|
|
481
|
+
|
|
482
|
+
// State
|
|
483
|
+
isReady,
|
|
484
|
+
isLoading,
|
|
485
|
+
error,
|
|
486
|
+
models,
|
|
487
|
+
|
|
488
|
+
// Client (for advanced use)
|
|
489
|
+
client,
|
|
490
|
+
|
|
491
|
+
// Chat methods
|
|
492
|
+
chat,
|
|
493
|
+
chatStream: chatStreamImpl,
|
|
494
|
+
|
|
495
|
+
// File upload
|
|
496
|
+
uploadFile,
|
|
497
|
+
|
|
498
|
+
// Data fetching
|
|
499
|
+
loadModels,
|
|
500
|
+
|
|
501
|
+
// Settings
|
|
502
|
+
selectedModel,
|
|
503
|
+
setSelectedModel,
|
|
504
|
+
systemPrompt,
|
|
505
|
+
setSystemPrompt,
|
|
506
|
+
skipServerPrompt,
|
|
507
|
+
setSkipServerPrompt,
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<HustleContext.Provider value={value}>
|
|
512
|
+
{children}
|
|
513
|
+
</HustleContext.Provider>
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Hook to access Hustle context
|
|
519
|
+
* Must be used within HustleProvider (which must be within EmblemAuthProvider)
|
|
520
|
+
*
|
|
521
|
+
* @example
|
|
522
|
+
* ```tsx
|
|
523
|
+
* function ChatComponent() {
|
|
524
|
+
* const { isReady, chat, chatStream } = useHustle();
|
|
525
|
+
*
|
|
526
|
+
* if (!isReady) {
|
|
527
|
+
* return <div>Please connect to start chatting</div>;
|
|
528
|
+
* }
|
|
529
|
+
*
|
|
530
|
+
* // Use chat or chatStream...
|
|
531
|
+
* }
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
export function useHustle(): HustleContextValue {
|
|
535
|
+
const context = useContext(HustleContext);
|
|
536
|
+
if (context === undefined) {
|
|
537
|
+
throw new Error('useHustle must be used within a HustleProvider (which requires EmblemAuthProvider)');
|
|
538
|
+
}
|
|
539
|
+
return context;
|
|
540
|
+
}
|