Coder Social home page Coder Social logo

rgommezz / react-native-chatgpt Goto Github PK

View Code? Open in Web Editor NEW
542.0 542.0 71.0 1.09 MB

A React Native wrapper around ChatGPT to seamlessly integrate it with your applications. It handles authentication, streamed responses, and keeping track of conversations. 100% client-side :robot:

License: MIT License

JavaScript 6.31% TypeScript 93.69%
chatbot chatgpt react react-native

react-native-chatgpt's People

Contributors

rgommezz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-native-chatgpt's Issues

Error 403 Forbidden

Hi, I wrote an example like yours and after success authorization I get 403 error,

    try {
      const { message, conversationId, messageId } = await sendMessage('whats foodApp ?');
      const { message: followUp } = await sendMessage('whats Doordash App', {
        conversationId,
        messageId,
      });
      setMessage(message);
    } catch (error) {
      if (error instanceof ChatGptError) {
        // Handle error accordingly
      }
    }
  };

using ChatGPT API key without login

Hi, thanks for useful library.
I just want to use the library without login, it's mean I will provide the API key, does this library support this feature? Thanks

Adding System Messages

I was attempting to add system messages to the repository to allow for prompt engineering. However, attempts are turning up flat. Any advice? Likely linked to lack of knowledge of codebase but working through it I'm not finding where else I'd need to make changes. Find below proposed changes to src/api/index.ts file

import { ChatGptError, ChatGptResponse, SendMessageParams } from '../types';
import {
  CHAT_PAGE,
  HOST_URL,
  LOGIN_PAGE,
  PROMPT_ENDPOINT,
  REQUEST_DEFAULT_TIMEOUT,
  STREAMED_REQUEST_DEFAULT_TIMEOUT,
} from '../constants';
import parseStreamedGptResponse from '../utils/parseStreamedGptResponse';
import getChatGptConversationHeaders from '../utils/getChatGptConversationHeaders';
import type { RefObject } from 'react';
import type WebView from 'react-native-webview';
import wait from '../utils/wait';
import { getStatusText } from '../utils/httpCodes';

let webview: RefObject<WebView>['current'];

export const init = (webviewRef: RefObject<WebView>['current']) => {
  webview = webviewRef;
};

/**
 * Monkey patches fetch to intercept ChatGPT requests and read the JWT
 * It also injects 2 methods in the global scope to accomplish the following:
 * 1. Sending messages to the ChatGPT backend directly from the Webview and stream the response back to RN
 * 2. Removing the theme switcher button from the webview when GPT shows it's at full capacity
 *
 * Note: It'd be cool to define the function in normal JS and
 * use fn.toString() or`${fn}` and wrap it in a IIFE,
 * but babel messes up the transformations of async/await and breaks the injected code.
 */
const systemPrompt = 'You are a baby name generator';

export const createGlobalFunctionsInWebviewContext = () => {
  return `
    const { fetch: originalFetch } = window;
    window.fetch = async (...args) => {
      const [resource, config] = args;
      window.ReactNativeWebView.postMessage(JSON.stringify({type: 'REQUEST_INTERCEPTED_CONFIG', payload: config}));
      const response = await originalFetch(resource, config);
      return response;
    };

    window.removeThemeSwitcher = () => {
      const svgIcon = document.querySelector("button > svg");
      if (!svgIcon) {
        return;
      }
      const themeSwitchButton = svgIcon.closest('button');
      if (themeSwitchButton) {
        themeSwitchButton.style.display = 'none';
      }
    };
    
  window.sendGptMessage = async ({
      accessToken,
      message,
      messageId,
      newMessageId,
      newSystemMessageId,
      conversationId,
      prompt,
      systemPrompt,
      timeout
    }) => {

      async function* streamAsyncIterable(stream) {
        const reader = stream.getReader()
        try {
          while (true) {
            const { done, value } = await reader.read()
            if (done) {
              return
            }
            yield value
          }
        } finally {
          reader.releaseLock()
        }
      }

      function getHeaders(accessToken) {
        return {
          accept: "text/event-stream",
          "x-openai-assistant-app-id": "",
          authorization: accessToken,
          "content-type": "application/json",
          origin: "${HOST_URL}",
          referrer: "${CHAT_PAGE}",
          "sec-fetch-mode": "cors",
          "sec-fetch-site": "same-origin",
          "x-requested-with": "com.chatgpt3auth"
        };
      }

      const url = "${PROMPT_ENDPOINT}";
      const body = {
        action: "next",
        messages: [
        {
            role: "system",
            content: {
              content_type: "text",
              parts: [systemPrompt],
            },
         },
         {
            id: newMessageId,
            role: "user",
            content: {
              content_type: "text",
              parts: [message],
            },
          },
        ],
        model: "gpt-3.5-turbo-0613",
        parent_message_id: messageId,
      };

      if (conversationId) {
        body.conversation_id = conversationId;
      }

      const headers = getHeaders(accessToken);

      try {

        const controller = new AbortController();

        const timeoutId = setTimeout(() => {
          // Notifying RN that the request timed out
          window.ReactNativeWebView.postMessage(JSON.stringify({type: 'STREAM_ERROR', payload: {status: 408, statusText: 'Request timed out'}}));
          controller.abort();
        }, timeout);

        const res = await fetch(url, {
          method: "POST",
          body: JSON.stringify(body),
          headers: headers,
          mode: "cors",
          credentials: "include",
          signal: controller.signal
        });

        clearTimeout(timeoutId);

        if (res.status >= 400 && res.status < 600) {
          window.ReactNativeWebView.postMessage(JSON.stringify({type: 'STREAM_ERROR', payload: {status: res.status}}));
          return true;
        }

        for await (const chunk of streamAsyncIterable(res.body)) {
          const str = new TextDecoder().decode(chunk);
          window.ReactNativeWebView.postMessage(JSON.stringify({type: 'RAW_ACCUMULATED_RESPONSE', payload: str}));
        }
      } catch (e) {
        // Nothing to do here
      }
    };

    true;
  `;
};

/**
 * Calls a global function in the Webview window object to send a streamed message
 */
export function postStreamedMessage({
  accessToken,
  message,
  messageId = uuid.v4() as string,
  conversationId,
  timeout = STREAMED_REQUEST_DEFAULT_TIMEOUT,
}: SendMessageParams) {
  const newMessageId = uuid.v4() as string;
  const newSystemMessageId = uuid.v4() as string;
  let script = '';
  if (conversationId) {
    script = `
      window.sendGptMessage({
        accessToken: "${accessToken}",
        message: "${message}",
        messageId: "${messageId}",
        newMessageId: "${newMessageId}",
        conversationId: "${conversationId}",
        systemPrompt: "${systemPrompt.replace(/"/g, '\\"')}",
        timeout: ${timeout}
      });

      true;
    `;
  } else {
    script = `
      window.sendGptMessage({
        accessToken: "${accessToken}",
        message: "${message}",
        messageId: "${messageId}",
        newMessageId: "${newMessageId}",
        systemPrompt: "${systemPrompt}",        
        timeout: ${timeout}
      });

      true;
    `;
  }

  webview?.injectJavaScript(script);
}

/**
 * Sends a normal message to the ChatGPT conversation backend endpoint
 */
export async function postMessage({
  accessToken,
  message,
  messageId = uuid.v4() as string,
  conversationId,
  systemPrompt,
  timeout = REQUEST_DEFAULT_TIMEOUT,
  onTokenExpired,
}: SendMessageParams): Promise<ChatGptResponse> {
  const controller = new AbortController();
  const newMessageId = uuid.v4() as string;
  const newSystemMessageId = uuid.v4() as string;
  
  const timeoutId = setTimeout(() => {
    controller.abort();
    const error = new ChatGptError(
      'ChatGPTResponseClientError: Request timed out'
    );
    error.statusCode = 408;
    throw error;
  }, timeout);

  const url = PROMPT_ENDPOINT;
  const body = {
    action: 'next',
    messages: [
     {
         role: "system",
         content: {
           content_type: "text",
           parts: [systemPrompt],
       },
     },
     {
        id: newMessageId,
        role: 'user',
        content: {
          content_type: 'text',
          parts: [message],
        },
      },
    ],
    model: 'gpt-3.5-turbo-0613',
    parent_message_id: messageId,
  };

  if (conversationId) {
    // @ts-ignore
    body.conversation_id = conversationId;
  }

  const res = await fetch(url, {
    method: 'POST',
    body: JSON.stringify(body),
    headers: getChatGptConversationHeaders(accessToken),
    mode: 'cors',
    signal: controller.signal,
  });

  clearTimeout(timeoutId);

  if (res.status >= 400 && res.status < 500) {
    if (res.status === 401) {
      // Token expired, notifying
      onTokenExpired?.();
    } else if (res.status === 403) {
      // Session expired, reloading Web View
      reloadWebView();
    }

    const error = new ChatGptError(getStatusText(res.status as any));
    error.statusCode = res.status;
    throw error;
  } else if (res.status >= 500) {
    const error = new ChatGptError(
      `ChatGPTResponseServerError: ${res.status} ${res.statusText}`
    );
    error.statusCode = res.status;
    throw error;
  }

  const rawText = await res.text();
  const parsedData = parseStreamedGptResponse(rawText);

  if (!parsedData) {
    throw new ChatGptError('ChatGPTResponseError: Unable to parse response');
  }

  return parsedData;
}

export function reloadWebView() {
  webview?.reload();
}

/**
 * Removes the icon button in the top right corner of the webview screen when
 * ChatGPT is at full capacity
 */
export async function removeThemeSwitcher() {
  // Apparently the button is not there yet after the page loads, so we wait a bit
  await wait(200);

  const script = `
    (() => {
      const xpath = "//div[contains(text(),'ChatGPT is at capacity right now')]";
      const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
      if (element) {
        window.removeThemeSwitcher();
      }
    })();

    true;
  `;

  webview?.injectJavaScript(script);
}

/**
 * Checks if ChatGPT servers are overloaded and the normal login page is not accessible
 */
export function checkFullCapacity() {
  const script = `
    (() => {
      const xpath = "//div[contains(text(),'ChatGPT is at capacity right now')]";
      const element = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
      if (element) {
        window.removeThemeSwitcher();
        window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'CHAT_GPT_FULL_CAPACITY' }));
      }
    })();

    true;
  `;
  webview?.injectJavaScript(script);
}

/**
 * Refreshes the webview and checks again if the login page is available
 */
export async function retryLogin() {
  reloadWebView();
  // Waiting 3 seconds before checking again
  await wait(3000);
  checkFullCapacity();
}

export function navigateToLoginPage() {
  const script = `
   (() => {
      window.location.replace("${LOGIN_PAGE}");
   })();

   true;
  `;

  webview?.injectJavaScript(script);
}

login view not hide after login; last message does not always contain message in stream mode.

Hi! ๐Ÿ‘‹

Firstly, thanks for your work on this project! ๐Ÿ™‚

Today I used patch-package to patch [email protected] for the project I'm working on.

Here is the diff that solved my problem:

diff --git a/node_modules/react-native-chatgpt/src/components/ChatGpt.tsx b/node_modules/react-native-chatgpt/src/components/ChatGpt.tsx
index f1b3f62..c2ff3e7 100644
--- a/node_modules/react-native-chatgpt/src/components/ChatGpt.tsx
+++ b/node_modules/react-native-chatgpt/src/components/ChatGpt.tsx
@@ -113,6 +113,7 @@ export default function ChatGpt({
         onAccessTokenChange={(token) => {
           setIsWaitingForJWT(false);
           setAccessToken(token);
+          modalRef?.current?.close();
         }}
         onAccumulatedResponse={(result) => callbackRef.current?.(result)}
         onStreamError={(error) => errorCallbackRef.current?.(error)}
diff --git a/node_modules/react-native-chatgpt/src/components/ModalWebView.tsx b/node_modules/react-native-chatgpt/src/components/ModalWebView.tsx
index dba7a4d..f85804d 100644
--- a/node_modules/react-native-chatgpt/src/components/ModalWebView.tsx
+++ b/node_modules/react-native-chatgpt/src/components/ModalWebView.tsx
@@ -44,6 +44,7 @@ type Props = PassedProps & PublicProps;
 
 export interface ModalWebViewMethods {
   open: () => void;
+  close: () => void;
 }
 
 const ModalWebView = forwardRef<ModalWebViewMethods, Props>(
@@ -80,7 +81,7 @@ const ModalWebView = forwardRef<ModalWebViewMethods, Props>(
     useImperativeHandle(ref, () => ({
       open: () => {
         animateWebView('show');
-      },
+      },close:()=>{animateWebView('hide');}
     }));
 
     useEffect(() => {
diff --git a/node_modules/react-native-chatgpt/src/utils/parseStreamedGptResponse.ts b/node_modules/react-native-chatgpt/src/utils/parseStreamedGptResponse.ts
index 736c607..42b9c2d 100644
--- a/node_modules/react-native-chatgpt/src/utils/parseStreamedGptResponse.ts
+++ b/node_modules/react-native-chatgpt/src/utils/parseStreamedGptResponse.ts
@@ -33,11 +33,17 @@ export default function parseStreamedGptResponse(data: string) {
     return null;
   }
   // @ts-ignore
-  const response = JSON.parse(sanitizedChunks[sanitizedChunks.length - 1]);
-  return {
-    message: response.message.content.parts[0],
-    messageId: response.message.id,
-    conversationId: response.conversation_id,
-    isDone: response.message?.end_turn === true,
-  };
+  for(let i=sanitizedChunks.length-1; i>-1;i--){
+    try{
+      const response = JSON.parse(sanitizedChunks[i]);
+      return {
+        message: response.message.content.parts[0],
+        messageId: response.message.id,
+        conversationId: response.conversation_id,
+        isDone: response.message?.end_turn === true,
+      };
+    }catch(e){
+      
+    }
+  }
 }

This issue body was partially generated by patch-package.

is it work?

In my case, it didn't work with error code 403 forbidden

False alarm on streamed calls?

I'm working on top of the provided example, I would like to use the normal call, not the streamed one.

          const response = await sendMessage(
            messages[1]?.text,
            messageId.current && conversationId.current
              ? {
                  messageId: messageId.current,
                  conversationId: conversationId.current,
                }
              : undefined,
          );

But I get the following error:

[TypeError: Cannot read property 'content' of undefined]

Which can be caused here

message: response.message.content.parts[0],

Is it being seen as a streamed call still?

Unable to resolve "@babel/runtime/helpers/interopRequireDefault" from "..\src\index.tsx"

โ€บ Scan the QR code above with Expo Go (Android) or the Camera app (iOS)

โ€บ Press a โ”‚ open Android
โ€บ Press w โ”‚ open web

โ€บ Press j โ”‚ open debugger
โ€บ Press r โ”‚ reload app
โ€บ Press m โ”‚ toggle menu

โ€บ Press ? โ”‚ show all commands

Logs for your project will appear below. Press Ctrl+C to exit.
Android Bundling failed 550ms
Unable to resolve "@babel/runtime/helpers/interopRequireDefault" from "..\src\index.tsx"

403 after first session timeout

After login, all is working until the session expires. At that point, the code should call reloadWebView due to 403 response, but instead the 403 response is seemingly doing nothing.

How to logout?

how to force a logout and go back to the beginning for a new login, I looked in the files but didn't find any function, currently I'm trying to clean or delete the token but without success.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.