Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Vue version of useAssistant hook #1678

Open
dosstx opened this issue May 23, 2024 · 1 comment
Open

Add a Vue version of useAssistant hook #1678

dosstx opened this issue May 23, 2024 · 1 comment
Labels
ai/ui enhancement New feature or request

Comments

@dosstx
Copy link
Contributor

dosstx commented May 23, 2024

Feature Description

According to the OPENAI ASSISTANTS page on the docs, "The useAssistant hook is currently supported with ai/react and ai/svelte".

As a Vue/Nuxt user, I would like a Vue version of the useAssistant hook (in Vue, they are called composables ).

Use Case

I have converted React's useAssistant hook to a Vue 3 version, but I am unsure how to import it into the current ai package for use:

import { ref, reactive, toRefs, onUnmounted } from 'vue';
import { isAbortError } from '@ai-sdk/provider-utils';
import { generateId } from '../shared/generate-id';
import { readDataStream } from '../shared/read-data-stream';
import {
  AssistantStatus,
  CreateMessage,
  Message,
  UseAssistantOptions,
} from '../shared/types';

export function useAssistant({
  api,
  threadId: threadIdParam,
  credentials,
  headers,
  body,
  onError,
}: UseAssistantOptions) {
  const state = reactive({
    messages: [] as Message[],
    input: '',
    threadId: undefined as string | undefined,
    status: 'awaiting_message' as AssistantStatus,
    error: undefined as unknown,
  });

  const abortControllerRef = ref<AbortController | null>(null);

  const handleInputChange = (event: Event) => {
    const target = event.target as HTMLInputElement | HTMLTextAreaElement;
    state.input = target.value;
  };

  const stop = () => {
    if (abortControllerRef.value) {
      abortControllerRef.value.abort();
      abortControllerRef.value = null;
    }
  };

  const append = async (
    message: Message | CreateMessage,
    requestOptions?: {
      data?: Record<string, string>;
    },
  ) => {
    state.status = 'in_progress';

    state.messages.push({
      ...message,
      id: message.id ?? generateId(),
    });

    state.input = '';

    const abortController = new AbortController();

    try {
      abortControllerRef.value = abortController;

      const result = await fetch(api, {
        method: 'POST',
        credentials,
        signal: abortController.signal,
        headers: { 'Content-Type': 'application/json', ...headers },
        body: JSON.stringify({
          ...body,
          threadId: threadIdParam ?? state.threadId ?? null,
          message: message.content,
          data: requestOptions?.data,
        }),
      });

      if (result.body == null) {
        throw new Error('The response body is empty.');
      }

      for await (const { type, value } of readDataStream(
        result.body.getReader(),
      )) {
        switch (type) {
          case 'assistant_message': {
            state.messages.push({
              id: value.id,
              role: value.role,
              content: value.content[0].text.value,
            });
            break;
          }

          case 'text': {
            const lastMessage = state.messages[state.messages.length - 1];
            lastMessage.content += value;
            break;
          }

          case 'data_message': {
            state.messages.push({
              id: value.id ?? generateId(),
              role: 'data',
              content: '',
              data: value.data,
            });
            break;
          }

          case 'assistant_control_data': {
            state.threadId = value.threadId;
            const lastMessage = state.messages[state.messages.length - 1];
            lastMessage.id = value.messageId;
            break;
          }

          case 'error': {
            state.error = new Error(value);
            break;
          }
        }
      }
    } catch (error) {
      if (isAbortError(error) && abortController.signal.aborted) {
        abortControllerRef.value = null;
        return;
      }

      if (onError && error instanceof Error) {
        onError(error);
      }

      state.error = error as Error;
    } finally {
      abortControllerRef.value = null;
      state.status = 'awaiting_message';
    }
  };

  const submitMessage = async (
    event?: Event,
    requestOptions?: {
      data?: Record<string, string>;
    },
  ) => {
    event?.preventDefault?.();

    if (state.input === '') {
      return;
    }

    append({ role: 'user', content: state.input }, requestOptions);
  };

  onUnmounted(() => {
    if (abortControllerRef.value) {
      abortControllerRef.value.abort();
    }
  });

  return {
    ...toRefs(state),
    handleInputChange,
    stop,
    append,
    submitMessage,
  };
}

Additional context

No response

@lgrammel lgrammel added ai/ui enhancement New feature or request labels May 23, 2024
@IllamosSalaman
Copy link

Would love to use useAssistant for Vue.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ai/ui enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants