npm.io
0.3.1 • Published 1 year ago

@rizean/poe-canvas-utils

Licence
Apache-2.0
Version
0.3.1
Deps
0
Size
268 kB
Vulns
0
Weekly
0
Stars
1

Poe.com Canvas Utils

codecov

Utility hooks and functions for building applications on the Poe platform, particularly for Poe Canvas Apps. This library provides tools for AI interaction, client-side logging, functional error handling, text filtering, and data persistence via file download/upload.

Features

  • usePoeAi: A React hook to interact with a Poe AI bot via the Poe Embed API, supporting streaming, attachments, and a robust simulation mode for development.
  • useLogger: A React hook for client-side logging with configurable levels and in-memory log storage, useful for debugging within the Poe Canvas environment.
  • tryCatchSync & tryCatchAsync: Utility functions to wrap synchronous or asynchronous operations for functional error handling, returning a Result tuple ([data, null] or [null, error]).
  • applyGeminiThinkingFilter: A utility function to clean up "Thinking..." artifacts from Gemini AI model responses.
  • saveDataToFile & loadDataFromFile: Utilities to allow users to save application data to a JSON file and load it back, with support for data versioning, migration, and validation.
  • Poe Types: Comprehensive TypeScript definitions for the Poe Embed API (window.Poe).

Installation

Replace @your-npm-username with the actual package name if published, or use a local path for local development. Assuming it's @rizean/poe-canvas-utils as per the project name:

Using pnpm:

pnpm add @rizean/poe-canvas-utils

Using npm:

npm install @rizean/poe-canvas-utils

Using yarn:

yarn add @rizean/poe-canvas-utils

Usage Examples

1. usePoeAi - Interacting with Poe Bots

This hook simplifies communication with Poe bots.

// MyPoeChatComponent.tsx
import React, { useState, useCallback } from 'react';
import { usePoeAi, type RequestState, type PoeMessage } from '@rizean/poe-canvas-utils';

function MyPoeChatComponent() {
  const [sendToPoe] = usePoeAi({
    // simulation: true, // Enable for development without Poe API
    // logger: console, // Optional: use your own logger or console
  });
  const [isLoading, setIsLoading] = useState(false);
  const [aiResponse, setAiResponse] = useState<PoeMessage | null>(null);
  const [error, setError] = useState<string | null>(null);

  const handleSubmitPrompt = useCallback((prompt: string) => {
    setIsLoading(true);
    setAiResponse(null);
    setError(null);

    const handlePoeUpdate = (state: RequestState) => {
      console.log('Poe AI State Update:', state);
      if (state.status === 'complete') {
        setIsLoading(false);
        if (state.responses && state.responses.length > 0) {
          setAiResponse(state.responses[state.responses.length -1]); // Get the last message
        }
      } else if (state.status === 'error') {
        setIsLoading(false);
        setError(state.error);
      } else if (state.status === 'incomplete') {
        // Handle streaming updates if stream: true is used
        if (state.responses && state.responses.length > 0) {
          setAiResponse(state.responses[state.responses.length -1]);
        }
      }
    };

    // Example: Ensure window.Poe is available or simulation is enabled.
    // The bot name (e.g., "@Gemini-2.5-Pro-Exp", "@Gemini-2.5-Flash") must be part of the prompt
    // if the bot requires it.
    sendToPoe(`@Gemini-2.5-Pro-Exp ${prompt}`, handlePoeUpdate, {
      // stream: true, // Optional: for streaming responses
      // openChat: false, // Optional: to prevent opening Poe chat UI
    });
  }, [sendToPoe]);

  // Basic form for input
  const [inputValue, setInputValue] = useState('');
  const handleFormSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (inputValue.trim()) {
      handleSubmitPrompt(inputValue.trim());
      setInputValue('');
    }
  };

  return (
    <div>
      <form onSubmit={handleFormSubmit}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="Ask a Poe bot (e.g., @Gemini-2.5-Pro-Exp What is React?)"
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading}>
          {isLoading ? 'Thinking...' : 'Send'}
        </button>
      </form>

      {aiResponse && (
        <div>
          <h4>AI Response:</h4>
          <p><strong>Sender:</strong> {aiResponse.senderId}</p>
          <p><strong>Content:</strong></p>
          <pre>{aiResponse.content}</pre>
          {aiResponse.attachments && aiResponse.attachments.length > 0 && (
            <div>
              <strong>Attachments:</strong>
              <ul>
                {aiResponse.attachments.map(att => (
                  <li key={att.attachmentId}><a href={att.url} target="_blank" rel="noopener noreferrer">{att.name}</a></li>
                ))}
              </ul>
            </div>
          )}
        </div>
      )}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}
    </div>
  );
}

export default MyPoeChatComponent;
2. usePoeAiTextGenerator - Interacting with Poe AI Text Generator Models

This hook is a specialized version of usePoeAi tailored for text generation use cases. It always streams responses and simplifies handling of text content, with optional parsing.

// MyTextGeneratorComponent.tsx
import React, { useState, useCallback } from 'react';
import {
    usePoeAiTextGenerator,
    type TextRequestState,
    type TextRequestCallback,
    type Result // For custom parser
} from '@rizean/poe-canvas-utils';

// Optional: Define a type for your parsed data if using a parser
interface MyParsedData {
  summary: string;
  keywords: string[];
}

// Optional: Define a custom parser function
const myCustomParser = (text: string): Result<MyParsedData, Error> => {
  try {
    // This is a very basic parser example.
    // In a real scenario, you might look for specific structures or use more robust parsing.
    if (text.length < 10) { // Simulate condition where parsing isn't possible yet or fails
        // For incomplete streams, you might return [null, null] or a specific error
        // if you expect more data before parsing is valid.
        // Or, if it's a definitive parse error:
        // return [null, new Error("Text too short to parse meaningful data.")];
    }
    // Let's assume the AI responds with "Summary: [text] Keywords: [kw1, kw2]"
    const summaryMatch = text.match(/Summary: (.*?)( Keywords:|$)/s);
    const keywordsMatch = text.match(/Keywords: (.*)/s);
    const summary = summaryMatch ? summaryMatch[1].trim() : "No summary found.";
    const keywords = keywordsMatch ? keywordsMatch[1].split(',').map(kw => kw.trim()) : [];

    if (!summaryMatch && !keywordsMatch && text.length > 0) {
        // If no specific structure is found, but there's text,
        // you might decide it's not an error, but just not parseable into MyParsedData.
        // Depending on strictness, you could return an error or a default/empty structure.
    }

    return [{ summary, keywords }, null];
  } catch (e) {
    return [null, e instanceof Error ? e : new Error('Unknown parsing error')];
  }
};

function MyTextGeneratorComponent() {
  const [sendTextPrompt] = usePoeAiTextGenerator<MyParsedData>({
    // simulation: true, // Enable for development
    // logger: console,
  });

  const [isGenerating, setIsGenerating] = useState(false);
  const [generatedText, setGeneratedText] = useState('');
  const [parsedData, setParsedData] = useState<MyParsedData | null>(null);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);

  const handleGenerateText = useCallback((prompt: string) => {
    setIsGenerating(true);
    setGeneratedText('');
    setParsedData(null);
    setErrorMessage(null);

    const callback: TextRequestCallback<MyParsedData> = (state) => {
      // console.log('Text Generator State:', state);
      setGeneratedText(state.text); // Always update with the latest raw text stream

      if (state.error) {
        setIsGenerating(false);
        setErrorMessage(state.error);
      } else if (state.parsed) {
        setParsedData(state.parsed);
        // You might set isGenerating to false here if parsing indicates completion,
        // or wait for the generating flag.
      }

      if (!state.generating && !state.error) {
        setIsGenerating(false);
        // Final state, even if parsing didn't yield data or wasn't used.
      }
    };

    // Example: Ensure window.Poe is available or simulation is enabled.
    sendTextPrompt(
      `@Gemini-2.5-Pro-Exp Summarize this and list keywords: ${prompt}`,
      callback,
      {
        parser: myCustomParser, // Provide the parser
        // simulatedResponseOverride: [{ messageId: 'sim1', content: 'Summary: Test. Keywords: A, B', ...}],
      }
    );
  }, [sendTextPrompt]);

  // Basic form for input
  const [inputValue, setInputValue] = useState('');
  const handleFormSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (inputValue.trim()) {
      handleGenerateText(inputValue.trim());
    }
  };

  return (
    <div>
      <form onSubmit={handleFormSubmit}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="Enter text to process..."
          disabled={isGenerating}
        />
        <button type="submit" disabled={isGenerating}>
          {isGenerating ? 'Generating...' : 'Generate Text'}
        </button>
      </form>

      {errorMessage && <p style={{ color: 'red' }}>Error: {errorMessage}</p>}

      <h4>Live Text Stream:</h4>
      <pre style={{ whiteSpace: 'pre-wrap', border: '1px solid #ccc', padding: '10px' }}>
        {generatedText || "Waiting for generation..."}
      </pre>

      {parsedData && (
        <div>
          <h4>Parsed Data:</h4>
          <p><strong>Summary:</strong> {parsedData.summary}</p>
          <p><strong>Keywords:</strong> {parsedData.keywords.join(', ')}</p>
        </div>
      )}
    </div>
  );
}

export default MyTextGeneratorComponent;
3. usePoeAiMediaGenerator - Interacting with Poe AI Media Generator Models

This hook is tailored for interactions that primarily result in media attachments (e.g., image generation bots). It defaults to non-streaming.

// MyMediaGeneratorComponent.tsx
import React, { useState, useCallback } from 'react';
import {
    usePoeAiMediaGenerator,
    type MediaRequestState,
    type MediaRequestCallback,
    type PoeMessageAttachment // Type for received attachments
} from '@rizean/poe-canvas-utils';

function MyMediaGeneratorComponent() {
  const [sendMediaPrompt] = usePoeAiMediaGenerator({
    // simulation: true,
    // logger: console,
  });

  const [isGenerating, setIsGenerating] = useState(false);
  const [attachments, setAttachments] = useState<PoeMessageAttachment[]>([]);
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
  const [inputFiles, setInputFiles] = useState<File[]>([]); // For sending attachments

  const handleGenerateMedia = useCallback((prompt: string) => {
    setIsGenerating(true);
    setAttachments([]);
    setErrorMessage(null);

    const callback: MediaRequestCallback = (state) => {
      // console.log('Media Generator State:', state);
      setIsGenerating(state.generating); // Reflect generating state

      if (state.error) {
        setErrorMessage(state.error);
      } else if (state.mediaAttachments.length > 0) {
        setAttachments(state.mediaAttachments);
      }
      // When state.generating becomes false and no error, generation is complete.
    };

    sendMediaPrompt(
      `@ImageCreatorBot Create an image of: ${prompt}`,
      callback,
      {
        attachments: inputFiles, // Send user-selected files with the prompt
        // openChat: true, // Optional
        // simulatedResponseOverride: [{ messageId: 'sim1', attachments: [{ attachmentId: 'a1', name: 'sim.png', url: '...', mimeType: 'image/png' }], ...}]
      }
    );
  }, [sendMediaPrompt, inputFiles]);

  const [inputValue, setInputValue] = useState('');
  const handleFormSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (inputValue.trim()) {
      handleGenerateMedia(inputValue.trim());
    }
  };

  const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.files) {
      setInputFiles(Array.from(event.target.files));
    }
  };

  return (
    <div>
      <form onSubmit={handleFormSubmit}>
        <input
          type="text"
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          placeholder="Enter media generation prompt..."
          disabled={isGenerating}
        />
        <br />
        <label>
          Optional files to send with prompt:
          <input type="file" multiple onChange={handleFileChange} disabled={isGenerating} />
        </label>
        <br />
        <button type="submit" disabled={isGenerating}>
          {isGenerating ? 'Generating Media...' : 'Generate Media'}
        </button>
      </form>

      {errorMessage && <p style={{ color: 'red' }}>Error: {errorMessage}</p>}

      {attachments.length > 0 && (
        <div>
          <h4>Generated Media:</h4>
          <ul>
            {attachments.map(att => (
              <li key={att.attachmentId}>
                <a href={att.url} target="_blank" rel="noopener noreferrer">{att.name}</a>
                ({att.mimeType})
                {att.mimeType.startsWith('image/') && <img src={att.url} alt={att.name} style={{maxWidth: '200px', display: 'block'}} />}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

export default MyMediaGeneratorComponent;
4. useLogger - Client-Side Logging

Useful for debugging your canvas app. Logs are stored in memory.

// MyLoggerDemoComponent.tsx
import React, { useEffect } from 'react';
import { useLogger, type LogEntry } from '@rizean/poe-canvas-utils';

function MyLoggerDemoComponent() {
  // Initialize logger, optionally set a log level ('trace', 'debug', 'info', 'warn', 'error')
  // Defaults to 'info' if not specified or invalid.
  const { logger, logs } = useLogger('debug');

  useEffect(() => {
    logger.info('LoggerComponent mounted.', { timestamp: new Date().toLocaleTimeString() });
    logger.debug('This is a debug message with some data:', { userId: 123, action: 'load' });
    logger.warn('A warning message occurred.');
    // logger.error('An example error!', new Error('Something went wrong'));
    logger.trace('This trace message will only show if logLevel is "trace".');
  }, [logger]);

  return (
    <div>
      <h3>Application Logs (In-Memory):</h3>
      {logs.length === 0 ? (
        <p>No logs yet.</p>
      ) : (
        <ul style={{ listStyleType: 'none', padding: 0 }}>
          {logs.map((log: LogEntry, index: number) => (
            <li key={index} style={{ marginBottom: '5px', borderBottom: '1px solid #eee', paddingBottom: '5px' }}>
              <span style={{ fontWeight: 'bold' }}>[{new Date(log.timestamp).toLocaleTimeString()}]</span>
              <span style={{ marginLeft: '8px', color: log.type === 'error' ? 'red' : log.type === 'warn' ? 'orange' : 'inherit' }}>
                [{log.type.toUpperCase()}]
              </span>
              <pre style={{ margin: '0 0 0 10px', display: 'inline-block', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
                {typeof log.message === 'string' ? log.message : JSON.stringify(log.message)}
              </pre>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default MyLoggerDemoComponent;
5. tryCatchSync and tryCatchAsync - Functional Error Handling

Wrap functions to handle errors without traditional try-catch blocks in your main logic.

// tryCatchExample.ts
import { tryCatchSync, tryCatchAsync, Result } from '@rizean/poe-canvas-utils';

// Synchronous example
function mightThrowSync(shouldThrow: boolean): string {
  if (shouldThrow) {
    throw new Error("Synchronous error!");
  }
  return "Success!";
}

const [dataSync, errorSync]: Result<string, Error> = tryCatchSync(() => mightThrowSync(true));

if (errorSync) {
  console.error("Caught sync error:", errorSync.message);
} else {
  console.log("Sync result:", dataSync);
}

// Asynchronous example
async function mightRejectAsync(shouldReject: boolean): Promise<string> {
  if (shouldReject) {
    return Promise.reject(new Error("Asynchronous error!"));
  }
  return Promise.resolve("Async Success!");
}

async function runAsyncExample() {
  const [dataAsync, errorAsync]: Result<string, Error> = await tryCatchAsync(() => mightRejectAsync(true));

  if (errorAsync) {
    console.error("Caught async error:", errorAsync.message);
  } else {
    console.log("Async result:", dataAsync);
  }
}

runAsyncExample();
6. applyGeminiThinkingFilter - Cleaning AI Output

Removes "Thinking..." blocks from text generated by Gemini models.

// geminiFilterExample.ts
import { applyGeminiThinkingFilter } from '@rizean/poe-canvas-utils';

const rawGeminiOutput = `
Some initial text.
*Thinking...*
> This is part of the thinking process.
> More thinking steps.

This is the actual response after thinking.
`;

const cleanedOutput = applyGeminiThinkingFilter(rawGeminiOutput);
console.log("Raw Output:\n", rawGeminiOutput);
console.log("Cleaned Output:\n", cleanedOutput);
// Expected Cleaned Output:
// "This is the actual response after thinking."
// (Leading/trailing newlines might vary based on exact input)
7. saveDataToFile and loadDataFromFile - Data Persistence

Allows users to download their application state as a JSON file and upload it later. This is crucial for Poe Canvas Apps which lack traditional browser storage.

// MyDataPersistenceComponent.tsx
import React, { useState, useCallback } from 'react';
import {
    saveDataToFile,
    loadDataFromFile,
    type VersionedData,
    type StorageLoadOptions
} from '@rizean/poe-canvas-utils';

// Define your application's data structure
interface AppDataV1 extends VersionedData {
    version: 1;
    notes: string[];
    settings: { theme: string };
}

interface AppDataV2 extends VersionedData {
    version: 2; // New version
    userNotes: Array<{ id: string; text: string }>; // Changed structure
    preferences: {
        theme: string;
        fontSize: number;
    };
}

// Current version of your app's data
const CURRENT_APP_VERSION = 2;
const DATA_FILENAME = 'my-app-data.json';

function MyDataPersistenceComponent() {
  const [appData, setAppData] = useState<AppDataV2 | null>(null);
  const [statusMessage, setStatusMessage] = useState('');

  const handleSaveData = useCallback(() => {
    if (!appData) {
      setStatusMessage('No data to save.');
      return;
    }
    const [success, error] = saveDataToFile(DATA_FILENAME, appData);
    if (success) {
      setStatusMessage(`Data saved to ${DATA_FILENAME}!`);
    } else {
      setStatusMessage(`Error saving data: ${error?.message}`);
    }
  }, [appData]);

  const handleLoadData = useCallback(async () => {
    const loadOptions: StorageLoadOptions<AppDataV2> = {
      currentVersion: CURRENT_APP_VERSION,
      migrate: async (loadedUntypedData: any, loadedVersion: number): Promise<AppDataV2> => {
        setStatusMessage(`Migrating data from v${loadedVersion} to v${CURRENT_APP_VERSION}...`);
        if (loadedVersion === 1) {
          // Example: Migrate from V1 to V2
          const oldData = loadedUntypedData as AppDataV1;
          return {
            version: 2,
            userNotes: oldData.notes.map((note, index) => ({ id: `note-${index}`, text: note })),
            preferences: {
              theme: oldData.settings.theme,
              fontSize: 14, // New default
            },
          };
        }
        // If more versions, add more migration steps here
        throw new Error(`Migration from version ${loadedVersion} not supported.`);
      },
      validate: async (dataToValidate: AppDataV2): Promise<boolean> => {
        // Example validation: ensure preferences exist
        const isValid = dataToValidate.preferences && typeof dataToValidate.preferences.theme === 'string';
        if (!isValid) setStatusMessage('Validation failed: Invalid preferences structure.');
        return isValid;
      },
    };

    const [loadedData, error] = await loadDataFromFile<AppDataV2>(loadOptions);

    if (error) {
      setStatusMessage(`Error loading data: ${error.message}`);
      setAppData(null);
    } else if (loadedData) {
      setAppData(loadedData);
      setStatusMessage('Data loaded successfully!');
    } else {
      setStatusMessage('File selection cancelled or no data loaded.');
    }
  }, []);

  // Initialize with some default data for V2
  useEffect(() => {
    setAppData({
        version: CURRENT_APP_VERSION,
        userNotes: [{id: '1', text: "Hello World"}],
        preferences: { theme: 'dark', fontSize: 16}
    });
  }, []);


  return (
    <div>
      <h3>Data Persistence Example</h3>
      <button onClick={handleSaveData} disabled={!appData}>Save Data to File</button>
      <button onClick={handleLoadData}>Load Data from File</button>
      {statusMessage && <p><i>{statusMessage}</i></p>}

      <h4>Current App Data:</h4>
      <pre>{appData ? JSON.stringify(appData, null, 2) : 'No data loaded.'}</pre>
    </div>
  );
}

export default MyDataPersistenceComponent;
8. complexResponseParser - Parsing Complex AI Responses

A utility function to parse structured AI responses that might contain a main textual part enclosed in configurable tags and an optional JSON data block.

// complexParserExample.ts
import { complexResponseParser, type ParsedComplexAiResponse, type ComplexParserOptions } from '@rizean/poe-canvas-utils';

const exampleAiResponseDefaultTags = `
Some initial chatter from the AI.
*Thinking...*
> Okay, planning to respond.
<response>
This is the primary textual answer.
It can span multiple lines.
</response>
Follow-up text.
\`\`\`json
{
  "id": 123,
  "status": "completed",
  "details": {
    "itemsProcessed": 5,
    "warnings": ["Low accuracy on item 3"]
  }
}
\`\`\`
Final remarks.
`;

const exampleAiResponseCustomTags = `
AI is starting...
[CHAT_START]
Hello! This is the chat content.
[CHAT_END]
\`\`\`json
{"user": "guest", "session": "xyz789"}
\`\`\`
`;

const exampleAiResponseNoJson = `
<response>
Just a simple text response.
</response>
`;

const exampleAiResponseMalformedJson = `
<response>
Text part is okay.
</response>
\`\`\`json
{ "data": "value", "invalid: json }
\`\`\`
`;

// 1. Using default tags (<response>, </response>)
const [parsedDefault, errorDefault] = complexResponseParser(exampleAiResponseDefaultTags);

if (errorDefault) {
  console.error("Default Parser Error:", errorDefault.message);
} else if (parsedDefault) {
  console.log("Parsed with Default Tags:");
  console.log("  Response:", parsedDefault.response); // "This is the primary textual answer.\nIt can span multiple lines."
  console.log("  Data:", parsedDefault.data);
  // Data: { id: 123, status: "completed", details: { itemsProcessed: 5, warnings: ["Low accuracy on item 3"] } }
}

// 2. Using custom tags
const customOptions: ComplexParserOptions = {
  responseStartTag: "[CHAT_START]",
  responseEndTag: "[CHAT_END]"
};
const [parsedCustom, errorCustom] = complexResponseParser(exampleAiResponseCustomTags, customOptions);

if (errorCustom) {
  console.error("Custom Parser Error:", errorCustom.message);
} else if (parsedCustom) {
  console.log("\nParsed with Custom Tags:");
  console.log("  Response:", parsedCustom.response); // "Hello! This is the chat content."
  console.log("  Data:", parsedCustom.data); // Data: { user: "guest", session: "xyz789" }
}

// 3. Response with no JSON
const [parsedNoJson, errorNoJson] = complexResponseParser(exampleAiResponseNoJson);
if (parsedNoJson) {
  console.log("\nParsed with No JSON:");
  console.log("  Response:", parsedNoJson.response); // "Just a simple text response."
  console.log("  Data exists:", 'data' in parsedNoJson); // false
}

// 4. Response with malformed JSON
const [parsedMalformed, errorMalformed] = complexResponseParser(exampleAiResponseMalformedJson);
if (errorMalformed) {
  console.error("\nMalformed JSON Error:", errorMalformed.message); // Will show JSON parsing error
}

// This parser can be used with usePoeAiTextGenerator:
//
// import { usePoeAiTextGenerator } from '@rizean/poe-canvas-utils';
// import { complexResponseParser, type ParsedComplexAiResponse } from '@rizean/poe-canvas-utils';
//
// const [sendPrompt] = usePoeAiTextGenerator<ParsedComplexAiResponse>();
//
// sendPrompt("query", (state) => {
//   if (state.parsed) {
//     // state.parsed.response
//     // state.parsed.data
//   }
// }, { parser: complexResponseParser });
//
// // To use custom tags with the text generator:
// sendPrompt("query", (state) => { /* ... */ }, {
//   parser: (text) => complexResponseParser(text, { responseStartTag: "[S]", responseEndTag: "[E]" })
// });

API Reference

usePoeAi(options?: UseAiOptions)
  • Returns: [(prompt: string, callback: RequestCallback, requestOptions?: RequestOptions) => void]
  • sendToAI function to initiate requests.
  • UseAiOptions:
  • handler?: string: Custom handler name.
  • simulation?: boolean: Enable/disable simulation.
  • simulationDelay?: number: Delay for simulated responses.
  • simulateErrorChance?: number: Chance of simulated error (0-100).
  • simulationResponses?: Message[] | null: Default simulated messages.
  • logger?: Logger: Custom logger instance.
  • RequestCallback: (state: RequestState) => void
  • RequestState: { requestId, generating, error, responses, status }
  • RequestOptions: { stream?, openChat?, simulatedResponseOverride?, attachments? }
usePoeAiTextGenerator<T = undefined>(options?: UseAiTextOptions)
  • Returns: [(prompt: string, callback: TextRequestCallback<T>, requestOptions?: TextRequestOptions<T>) => Promise<void>]
    • A tuple containing a single function (typically named sendTextMessage or sendTextPrompt) to initiate text generation requests.
  • UseAiTextOptions: Extends PoeUseAiOptions (the options for the base usePoeAi hook). Allows configuring simulation, logger, etc., specifically for this text generator instance.
  • TextRequestCallback<T>: (state: TextRequestState<T>) => void
    • A callback function invoked with state updates during the text generation lifecycle. T is the type of the parsed data if a parser is used.
  • TextRequestState<T>:
    • requestId: string: Unique ID for the request.
    • generating: boolean: True if the AI is still generating text.
    • error: string | null: An error message if an error occurred (from AI or parser).
    • text: string: The raw (or partially streamed) text content from the AI.
    • parsed?: T: The output from the provided parser function, if any. T is the type of the parsed data.
    • rawResponse?: PoeAiRequestState | null: The raw state object from the underlying usePoeAi hook, for debugging or advanced use.
  • TextRequestOptions<T>:
    • simulatedResponseOverride?: PoeMessage[] | null: Specific simulated messages for this request, overriding global simulation settings.
    • parser?: (text: string) => Result<T, Error>: An optional function to parse the AI's text response. It should handle potentially incomplete text (during streaming) and return a Result tuple ([parsedData, null] on success, or [null, error] on failure).
usePoeAiMediaGenerator(options?: UseAiMediaOptions)
  • Returns: [(prompt: string, callback: MediaRequestCallback, requestOptions?: MediaRequestOptions) => Promise<void>]
    • A tuple containing a single function (typically named sendMediaMessage or sendMediaPrompt) to initiate media generation requests.
  • UseAiMediaOptions: Extends PoeUseAiOptions. Allows configuring simulation, logger, etc., for this media generator instance.
  • MediaRequestCallback: (state: MediaRequestState) => void
    • A callback function invoked with state updates during the media generation lifecycle.
  • MediaRequestState:
    • requestId: string: Unique ID for the request.
    • generating: boolean: True if the AI is still generating media.
    • error: string | null: An error message if an error occurred.
    • mediaAttachments: PoeMessageAttachment[]: An array of PoeMessageAttachment objects representing the generated media.
    • rawResponse?: PoeAiRequestState | null: The raw state object from the underlying usePoeAi hook.
  • MediaRequestOptions:
    • simulatedResponseOverride?: PoeMessage[] | null: Specific simulated messages for this request.
    • attachments?: File[]: An array of File objects to send with the prompt to the AI.
    • openChat?: boolean: Whether to attempt to open the Poe chat interface (defaults to false or as per usePoeAi default).
useLogger(logLevelInput?: string)
  • Returns: { logs: LogEntry[], logger: Logger }
  • logLevelInput: 'trace' | 'debug' | 'info' | 'warn' | 'error' (defaults to 'info').
  • LogEntry: { type: string, message: unknown, timestamp: Date }
  • Logger: Interface with methods like debug(), info(), error(), etc.
tryCatchSync<T, E = Error>(fn: () => T, mapError?: (caughtError: unknown) => E)
  • Returns: Result<T, E> which is [T, null] | [null, E]
tryCatchAsync<T, E = Error>(fn: () => Promise<T>, mapError?: (caughtError: unknown) => E)
  • Returns: Promise<Result<T, E>>
applyGeminiThinkingFilter(text: string)
  • Returns: string (filtered text)
saveDataToFile<T extends VersionedData>(filename: string, data: T)
  • Returns: Result<true, Error>
  • Triggers a file download.
loadDataFromFile<T extends VersionedData>(options: StorageLoadOptions<T>)
  • Returns: Promise<Result<T | null, Error | null>>
  • Prompts user for file upload.
  • StorageLoadOptions<T>:
  • currentVersion: number: Your application's current data structure version.
  • migrate?: (loadedData: any, loadedVersion: number) => T | Promise<T>: Function to migrate older data structures.
  • validate?: (data: T) => boolean | Promise<boolean>: Function to validate loaded (and possibly migrated) data.
  • VersionedData: Interface { version: number; [key: string]: any; } that your data structure must implement.
complexResponseParser(rawText: string, options?: ComplexParserOptions)
  • Returns: Result<ParsedComplexAiResponse, Error>
    • A Result tuple: [ParsedComplexAiResponse, null] on successful parsing, or [null, Error] if an error occurs (e.g., malformed JSON within a declared JSON block).
  • rawText: string: The raw string output from the AI to be parsed.
  • options?: ComplexParserOptions:
    • responseStartTag?: string: Custom string marking the beginning of the main response block (defaults to "<response>").
    • responseEndTag?: string: Custom string marking the end of the main response block (defaults to "</response>").
  • ParsedComplexAiResponse (Interface for the successfully parsed data):
    • response: string: The extracted textual content from between the response tags.
    • data?: unknown: The parsed data from the ```json ... ``` block, if present and valid. This key is absent if no JSON block is found.
Poe Types

The library exports various types from src/types/Poe.ts (e.g., PoeMessage, PoeMessageAttachment, PoeSendUserMessageResult, PoeEmbedAPIError) for strong typing when working with the Poe API. It also augments the global Window interface to include window.Poe.

Contributing

Contributions are welcome! Please open an issue or submit a pull request to the GitHub repository.

License

Apache-2.0 License