steven-tey / novel

Notion-style WYSIWYG editor with AI-powered autocompletion.
https://novel.sh
Apache License 2.0
11.8k stars 980 forks source link

TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))bug: #378

Open Davidthecode opened 3 months ago

Davidthecode commented 3 months ago

Provide environment information

System: OS: macOS 12.7.4 CPU: (4) x64 Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz Memory: 18.83 MB / 8.00 GB Shell: 5.8.1 - /bin/zsh Binaries: Node: 20.12.0 - /usr/local/bin/node npm: 10.5.0 - /usr/local/bin/npm pnpm: 8.15.6 - /usr/local/bin/pnpm

Describe the bug

Whenever i highlight a text and click on the ask AI option, i get an error that says - TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator). I can't exactly pinpoint the error because there isn't enough information about it. i did some debugging and i think the error might be coming from the ai-selector-commands component. I noticed that when i comment the code in the return block inside the ai-selector-commands component, i don't get the error but only the input with the ask AI to edit or generate option is displayed in the ui. however when i start typing in the input i get the error again. I have been trying hard to find the issue but i can't seem to figure it out.

here's the "ai-selector" component for reference -

"use client";

import { Command, CommandInput } from "../ui/command";
import { useCompletion } from "ai/react";
import { toast } from "sonner";
import { useEditor } from "novel";
import { useState } from "react";
import Markdown from "react-markdown";
import AISelectorCommands from "./ai-selector-commands";
import AICompletionCommands from "./ai-completion-command";
import { ScrollArea } from "../ui/scroll-area";
import { Button } from "../ui/button";
import { ArrowUp } from "lucide-react";
import Magic from "../ui/Icons/magic";
import CrazySpinner from "../ui/Icons/crazy-spinner";
import { addAIHighlight } from "novel/extensions";

type AISelectorProps = {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

export function AISelector({ open, onOpenChange }: AISelectorProps) {
  const { editor } = useEditor();
  const [inputValue, setInputValue] = useState("");

  const { completion, complete, isLoading } = useCompletion({
    api: "/api/generate",
    onResponse: (response) => {
      if (response.status === 429) {
        toast.error("You have reached your request limit for the day.");
        return;
      }
    },
    onError: (e) => {
      toast.error(e.message);
      console.log(e)
    },
  });

  const hasCompletion = completion.length > 0;

  return (
    <Command className="w-[350px]">
      {hasCompletion && (
        <div className="flex max-h-[400px]">
          <ScrollArea>
            <div className="prose p-2 px-4 prose-sm">
              <Markdown>{completion}</Markdown>
            </div>
          </ScrollArea>
        </div>
      )}

      {isLoading && (
        <div className="flex h-12 w-full items-center px-4 text-sm font-medium text-muted-foreground text-purple-500">
          <Magic className="mr-2 h-4 w-4 shrink-0  " />
          AI is thinking
          <div className="ml-2 mt-1">
            <CrazySpinner />
          </div>
        </div>
      )}
      {!isLoading && (
        <>
          <div className="relative">
            <CommandInput
              value={inputValue}
              onValueChange={setInputValue}
              autoFocus
              placeholder={
                hasCompletion
                  ? "Tell AI what to do next"
                  : "Ask AI to edit or generate..."
              }
              onFocus={() => editor && addAIHighlight(editor)}
            />
            <Button
              size="icon"
              className="absolute right-2 top-1/2 h-6 w-6 -translate-y-1/2 rounded-full bg-purple-500 hover:bg-purple-900"
              onClick={() => {
                if (completion)
                  return complete(completion, {
                    body: { option: "zap", command: inputValue },
                  }).then(() => setInputValue(""));

                const slice = editor && editor.state.selection.content();
                const text = editor && slice && editor.storage.markdown.serializer.serialize(
                  slice.content,
                );

                complete(text, {
                  body: { option: "zap", command: inputValue },
                }).then(() => setInputValue(""));
              }}
            >
              <ArrowUp className="h-4 w-4" />
            </Button>
          </div>
          {hasCompletion ? (
            <AICompletionCommands
              onDiscard={() => {
                editor && editor.chain().unsetHighlight().focus().run();
                onOpenChange(false);
              }}
              completion={completion}
            />
          ) : (
            <AISelectorCommands
              onSelect={(value, option) =>
                complete(value, { body: { option } })
              }
            />
          )}
        </>
      )}
    </Command>
  );
}

here is the "ai-selector-commands" component for reference -

import React from "react";
import { CommandGroup, CommandItem, CommandSeparator } from "../ui/command";
import {
  ArrowDownWideNarrow,
  CheckCheck,
  RefreshCcwDot,
  StepForward,
  WrapText,
} from "lucide-react";
import { useEditor } from "novel";
import { getPrevText } from "novel/extensions";

const options = [
  {
    value: "improve",
    label: "Improve writing",
    icon: RefreshCcwDot,
  },

  {
    value: "fix",
    label: "Fix grammar",
    icon: CheckCheck,
  },
  {
    value: "shorter",
    label: "Make shorter",
    icon: ArrowDownWideNarrow,
  },
  {
    value: "longer",
    label: "Make longer",
    icon: WrapText,
  },
];

type AISelectorCommandsProps = {
  onSelect: (value: string, option: string) => void;
}

const AISelectorCommands = ({ onSelect }: AISelectorCommandsProps) => {
  const { editor } = useEditor();

  return (
    <>
      <CommandGroup heading="Edit or review selection">
        {options.map((option) => (
          <CommandItem
            onSelect={(value) => {
              const slice = editor && editor.state.selection.content();
              const text = editor && editor.storage.markdown.serializer.serialize(
                slice && slice.content,
              );
              onSelect(text, value);
            }}
            className="flex gap-2 px-4"
            key={option.value}
            value={option.value}
          >
            <option.icon className="h-4 w-4 text-purple-500" />
            {option.label}
          </CommandItem>
        ))}
      </CommandGroup>
      <CommandSeparator />
      <CommandGroup heading="Use AI to do more">
        <CommandItem
          onSelect={() => {
            const text = editor && getPrevText(editor, { chars: 5000 });
            onSelect(text, "continue");
          }}
          value="continue"
          className="gap-2 px-4"
        >
          <StepForward className="h-4 w-4 text-purple-500" />
          Continue writing
        </CommandItem>
      </CommandGroup>
    </>
  );
};

export default AISelectorCommands;

here is the "ai-completion-command" for reference -

import React from "react";
import { CommandGroup, CommandItem, CommandSeparator } from "../ui/command";
import { useEditor } from "novel";
import { Check, TextQuote, TrashIcon } from "lucide-react";

type AICompletionCommandsType = {
  completion: string,
  onDiscard: () => void,
};

const AICompletionCommands = ({ completion, onDiscard }: AICompletionCommandsType) => {
  const { editor } = useEditor();
  return (
    <>
      <CommandGroup>
        <CommandItem
          className="gap-2 px-4"
          value="replace"
          onSelect={() => {
            const selection = editor && editor.view.state.selection;

            editor && selection && editor
              .chain()
              .focus()
              .insertContentAt(
                {
                  from: selection.from,
                  to: selection.to,
                },
                completion,
              )
              .run();
          }}
        >
          <Check className="h-4 w-4 text-muted-foreground" />
          Replace selection
        </CommandItem>
        <CommandItem
          className="gap-2 px-4"
          value="insert"
          onSelect={() => {
            const selection = editor && editor.view.state.selection;
            editor && selection && editor
              .chain()
              .focus()
              .insertContentAt(selection.to + 1, completion)
              .run();
          }}
        >
          <TextQuote className="h-4 w-4 text-muted-foreground" />
          Insert below
        </CommandItem>
      </CommandGroup>
      <CommandSeparator />

      <CommandGroup>
        <CommandItem onSelect={onDiscard} value="thrash" className="gap-2 px-4">
          <TrashIcon className="h-4 w-4 text-muted-foreground" />
          Discard
        </CommandItem>
      </CommandGroup>
    </>
  );
};

export default AICompletionCommands;

Link to reproduction

https://novel.sh

To reproduce

I get the error when i click on the Ask AI option

Additional information

No response

john-dw commented 3 months ago

Hey @Davidthecode, I have the exact same issue. Did you ever find out how to resolve it ? Thanks

john-dw commented 3 months ago

Downgrading cmdk package version to 0.2.1 did the trick. It works now.

Davidthecode commented 3 months ago

ok @john-dw, i'll try it out

RexanWONG commented 1 month ago

Downgrading cmdk package version to 0.2.1 did the trick. It works now.

Worked for me, thanks!

john-dw commented 1 month ago

From https://github.com/pacocoursey/cmdk/releases:

Command.List is now required (CommandList in shadcn)

Rendering the Command.List part (CommandList if using shadcn) is now mandatory. Otherwise, you should expect to see an error like this:

TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator)) The fix:

// Before
<Command label="Command Menu">
    <Command.Input />
    <Command.Item />
    {/* ... */}
</Command>
// After
<Command label="Command Menu">
    <Command.Input />

    <Command.List>
        <Command.Item />
        {/* ... */}
    </Command.List>
</Command>