npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rizean/poe-canvas-utils

v0.3.1

Published

Utility hooks for Poe Canvas integration and logging.

Readme

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