@flavoai/fastfold
Version:
Flavo frontend package
445 lines • 14.5 kB
JavaScript
/**
* FastFold AI Client Hooks
* React hooks for AI functionality with streaming support
*/
import { useState, useCallback, useRef, useEffect } from 'react';
// ============================================================================
// HTTP HELPERS
// ============================================================================
let baseUrl = '/api';
export function configureAIClient(config) {
if (config.baseUrl) {
baseUrl = config.baseUrl;
}
}
async function postJSON(url, data) {
const response = await fetch(`${baseUrl}${url}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`AI request failed: ${errorText}`);
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'AI request failed');
}
return result.data;
}
// ============================================================================
// AI CHAT HOOK
// ============================================================================
/**
* 💬 useAIChat - Chat with AI with streaming support
*
* @example
* const { messages, input, handleSubmit, isLoading, setInput } = useAIChat({
* api: '/api/ai/chat',
* system: 'You are a helpful assistant',
* onFinish: (response) => console.log('Done:', response),
* });
*
* // In your component:
* <form onSubmit={handleSubmit}>
* <input value={input} onChange={(e) => setInput(e.target.value)} />
* <button type="submit" disabled={isLoading}>Send</button>
* </form>
* {messages.map((m, i) => <div key={i}>{m.role}: {m.content}</div>)}
*/
export function useAIChat(options = {}) {
const { api = '/ai/chat', model, provider, system, temperature, maxTokens, initialMessages = [], onChunk, onFinish, onError, } = options;
const [messages, setMessages] = useState(initialMessages);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
const append = useCallback(async (message) => {
const newMessages = [...messages, message];
setMessages(newMessages);
if (message.role === 'user') {
setIsLoading(true);
setError(null);
try {
const response = await postJSON(api, {
messages: system
? [{ role: 'system', content: system }, ...newMessages]
: newMessages,
model,
provider,
temperature,
maxTokens,
});
const assistantMessage = {
role: 'assistant',
content: response.text,
};
setMessages(prev => [...prev, assistantMessage]);
onFinish?.(response);
return response;
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
onError?.(error);
throw error;
}
finally {
setIsLoading(false);
}
}
return null;
}, [messages, api, model, provider, system, temperature, maxTokens, onFinish, onError]);
const handleSubmit = useCallback(async (e) => {
e?.preventDefault();
if (!input.trim() || isLoading)
return;
const userMessage = { role: 'user', content: input.trim() };
setInput('');
await append(userMessage);
}, [input, isLoading, append]);
const stop = useCallback(() => {
abortControllerRef.current?.abort();
setIsLoading(false);
}, []);
const reload = useCallback(async () => {
if (messages.length === 0)
return;
// Find last user message
const lastUserIndex = messages.findLastIndex(m => m.role === 'user');
if (lastUserIndex === -1)
return;
// Remove messages from last user message onwards
const newMessages = messages.slice(0, lastUserIndex);
setMessages(newMessages);
// Re-send the last user message
const lastUserMessage = messages[lastUserIndex];
await append(lastUserMessage);
}, [messages, append]);
const clear = useCallback(() => {
setMessages(initialMessages);
setInput('');
setError(null);
}, [initialMessages]);
return {
messages,
input,
setInput,
handleSubmit,
append,
reload,
stop,
clear,
isLoading,
error,
};
}
// ============================================================================
// AI STREAM HOOK
// ============================================================================
/**
* 🌊 useAIStream - Stream text generation with SSE
*
* @example
* const { text, isStreaming, generate, stop } = useAIStream({
* api: '/api/ai/stream',
* onChunk: (chunk) => console.log('Chunk:', chunk),
* onFinish: (text) => console.log('Final:', text),
* });
*
* // Start streaming
* await generate({ prompt: 'Tell me a story' });
*/
export function useAIStream(options = {}) {
const { api = '/ai/stream', model, provider, system, temperature, maxTokens, onChunk, onFinish, onError, } = options;
const [text, setText] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const [error, setError] = useState(null);
const abortControllerRef = useRef(null);
const generate = useCallback(async (params) => {
setIsStreaming(true);
setError(null);
setText('');
abortControllerRef.current = new AbortController();
try {
const response = await fetch(`${baseUrl}${api}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...params,
model,
provider,
system,
temperature,
maxTokens,
}),
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
throw new Error(`Stream request failed: ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) {
throw new Error('No response body');
}
const decoder = new TextDecoder();
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done)
break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.text) {
fullText += data.text;
setText(fullText);
onChunk?.(data.text);
}
if (data.done) {
onFinish?.(fullText);
}
if (data.error) {
throw new Error(data.error);
}
}
catch (e) {
// Skip invalid JSON lines
}
}
}
}
return fullText;
}
catch (err) {
if (err.name === 'AbortError') {
return text;
}
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
onError?.(error);
throw error;
}
finally {
setIsStreaming(false);
}
}, [api, model, provider, system, temperature, maxTokens, onChunk, onFinish, onError, text]);
const stop = useCallback(() => {
abortControllerRef.current?.abort();
setIsStreaming(false);
}, []);
const clear = useCallback(() => {
setText('');
setError(null);
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
abortControllerRef.current?.abort();
};
}, []);
return {
text,
isStreaming,
error,
generate,
stop,
clear,
};
}
// ============================================================================
// AI OBJECT HOOK
// ============================================================================
/**
* 🎯 useAIObject - Generate structured objects with AI
*
* @example
* const { object, generate, isLoading, error } = useAIObject({
* schema: {
* type: 'object',
* properties: {
* name: { type: 'string' },
* age: { type: 'number' },
* },
* required: ['name', 'age'],
* },
* onFinish: (obj) => console.log('Generated:', obj),
* });
*
* // Generate object
* await generate({ prompt: 'Generate a random person' });
*/
export function useAIObject(options) {
const { api = '/ai/generate', schema, model, provider, temperature, maxTokens, system, onFinish, onError, } = options;
const [object, setObject] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const generate = useCallback(async (params) => {
setIsLoading(true);
setError(null);
try {
const response = await postJSON(api, {
...params,
schema,
model,
provider,
temperature,
maxTokens,
system,
});
setObject(response.object);
onFinish?.(response.object);
return response.object;
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
onError?.(error);
throw error;
}
finally {
setIsLoading(false);
}
}, [api, schema, model, provider, temperature, maxTokens, system, onFinish, onError]);
const clear = useCallback(() => {
setObject(null);
setError(null);
}, []);
return {
object,
isLoading,
error,
generate,
clear,
};
}
// ============================================================================
// AI COMPLETION HOOK
// ============================================================================
/**
* ✍️ useAICompletion - Simple text completion
*
* @example
* const { text, complete, isLoading } = useAICompletion({
* model: 'gpt-4o',
* });
*
* const result = await complete('Translate to French: Hello world');
*/
export function useAICompletion(options = {}) {
const { api = '/ai/complete', model, provider, temperature, maxTokens, onFinish, onError, } = options;
const [text, setText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const complete = useCallback(async (prompt) => {
setIsLoading(true);
setError(null);
try {
const response = await postJSON(api, {
prompt,
model,
provider,
temperature,
maxTokens,
});
setText(response.text);
onFinish?.(response.text);
return response.text;
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
onError?.(error);
throw error;
}
finally {
setIsLoading(false);
}
}, [api, model, provider, temperature, maxTokens, onFinish, onError]);
const clear = useCallback(() => {
setText('');
setError(null);
}, []);
return {
text,
isLoading,
error,
complete,
clear,
};
}
// ============================================================================
// AI EMBED HOOK
// ============================================================================
/**
* 🔢 useAIEmbed - Generate embeddings
*
* @example
* const { embed, embedMany, isLoading } = useAIEmbed();
*
* const embedding = await embed('Hello world');
* const embeddings = await embedMany(['Hello', 'World']);
*/
export function useAIEmbed(options = {}) {
const { api = '/ai/embed', model, provider, } = options;
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const embed = useCallback(async (text) => {
setIsLoading(true);
setError(null);
try {
const response = await postJSON(api, {
text,
model,
provider,
});
return response.embedding;
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
throw error;
}
finally {
setIsLoading(false);
}
}, [api, model, provider]);
const embedMany = useCallback(async (texts) => {
setIsLoading(true);
setError(null);
try {
const response = await postJSON(api, {
texts,
model,
provider,
});
return response.embeddings;
}
catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
setError(error);
throw error;
}
finally {
setIsLoading(false);
}
}, [api, model, provider]);
return {
isLoading,
error,
embed,
embedMany,
};
}
export default {
useAIChat,
useAIStream,
useAIObject,
useAICompletion,
useAIEmbed,
};
//# sourceMappingURL=ai.js.map