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

feat: add a search function in the treeview / improvement upload files and complete folders to chat from local #179

Closed
wants to merge 0 commits into from

Conversation

Stijnus
Copy link

@Stijnus Stijnus commented Nov 5, 2024

No description provided.

@Stijnus Stijnus closed this Nov 5, 2024
@Stijnus Stijnus reopened this Nov 5, 2024
@Stijnus Stijnus changed the title Add a search function in the treeview feat Add a search function in the treeview Nov 5, 2024
@Stijnus Stijnus changed the title feat Add a search function in the treeview feat: Add a search function in the treeview Nov 5, 2024
@Stijnus Stijnus changed the title feat: Add a search function in the treeview feat: Add a search function in the treeview / improvement upload files and complete folders to chat from local Nov 6, 2024
@Stijnus
Copy link
Author

Stijnus commented Nov 6, 2024

improved the folder upload from : #162
Thx to muzafferkadir

In progress :

  • Delete option / function
  • Upload progress bar
  • Exclude Node_Modules folder when uploading
  • Handling large files upload

@Stijnus Stijnus changed the title feat: Add a search function in the treeview / improvement upload files and complete folders to chat from local feat: add a search function in the treeview / improvement upload files and complete folders to chat from local Nov 6, 2024
@syndicate604
Copy link

syndicate604 commented Nov 6, 2024

Uploading folders fails all you get is blank files with
❯ cat index.css
[object Promise]

Also people need to know how to upload files into the two important dirs

project/
src/

If you just upload anything other than /src it will mess up your llm and the file structure.

Also this should be upgraded to allow multi-file without a folder for the initial files like vite-config etc if you are restoring a full build

Seems to work with these changes to chat.client.tsx

Await the file.text() promise in the addCustomFolder function
Use Promise.all to read all files concurrently in the addCustomFolder function

import { useStore } from '@nanostores/react';
import type { Message } from 'ai';
import { useChat } from 'ai/react';
import { useAnimate } from 'framer-motion';
import { memo, useEffect, useRef, useState } from 'react';
import { cssTransition, toast, ToastContainer } from 'react-toastify';
import { useMessageParser, usePromptEnhancer, useShortcuts, useSnapScroll } from '~/lib/hooks';
import { useChatHistory } from '~/lib/persistence';
import { chatStore } from '~/lib/stores/chat';
import { workbenchStore } from '~/lib/stores/workbench';
import { fileModificationsToHTML } from '~/utils/diff';
import { DEFAULT_MODEL } from '~/utils/constants';
import { cubicEasingFn } from '~/utils/easings';
import { createScopedLogger, renderLogger } from '~/utils/logger';
import { BaseChat } from './BaseChat';

const toastAnimation = cssTransition({
  enter: 'animated fadeInRight',
  exit: 'animated fadeOutRight',
});

const logger = createScopedLogger('Chat');

export function Chat() {
  renderLogger.trace('Chat');

  const { ready, initialMessages, storeMessageHistory } = useChatHistory();

  return (
    <>
      {ready && <ChatImpl initialMessages={initialMessages} storeMessageHistory={storeMessageHistory} />}
      <ToastContainer
        closeButton={({ closeToast }) => {
          return (
            <button className="Toastify__close-button" onClick={closeToast}>
              <div className="i-ph:x text-lg" />
            </button>
          );
        }}
        icon={({ type }) => {
          /**
           * @todo Handle more types if we need them. This may require extra color palettes.
           */
          switch (type) {
            case 'success': {
              return <div className="i-ph:check-bold text-bolt-elements-icon-success text-2xl" />;
            }
            case 'error': {
              return <div className="i-ph:warning-circle-bold text-bolt-elements-icon-error text-2xl" />;
            }
          }

          return undefined;
        }}
        position="bottom-right"
        pauseOnFocusLoss
        transition={toastAnimation}
      />
    </>
  );
}

interface ChatProps {
  initialMessages: Message[];
  storeMessageHistory: (messages: Message[]) => Promise<void>;
}

export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProps) => {
  useShortcuts();

  const textareaRef = useRef<HTMLTextAreaElement>(null);

  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
  const [model, setModel] = useState(DEFAULT_MODEL);

  const { showChat } = useStore(chatStore);

  const [animationScope, animate] = useAnimate();

  const { messages, isLoading, input, handleInputChange, setInput, stop, append } = useChat({
    api: '/api/chat',
    onError: (error) => {
      logger.error('Request failed\n\n', error);
      toast.error('There was an error processing your request');
    },
    onFinish: () => {
      logger.debug('Finished streaming');
    },
    initialMessages,
  });

  const { enhancingPrompt, promptEnhanced, enhancePrompt, resetEnhancer } = usePromptEnhancer();
  const { parsedMessages, parseMessages } = useMessageParser();

  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;

  const addCustomFile = async () => {
    const input = document.createElement('input');
    input.type = 'file';

    input.onchange = async (e) => {
      const file = e.target.files[0];
      const randomID = Math.random().toString(36).substring(2, 15);
      const fileName = file.name;

      const content = await file.text();

      const newMessage = {
        id: randomID,
        role: 'assistant',
        content: `File Added: ${fileName} <boltArtifact id="${randomID}" title="${fileName}">\n  <boltAction type="file" filePath="${fileName}">\n    ${content}\n  </boltAction>\n</boltArtifact>`,
        createdAt: Date.now(),
      };

      messages.push(newMessage);
      await storeMessageHistory(messages);
      parseMessages(messages, false);
    };

    input.click();
  };
  workbenchStore.addCustomFile = addCustomFile;

  const addCustomFolder = async () => {
    const input = document.createElement('input');
    input.type = 'file';
    input.webkitdirectory = true;
    input.multiple = true;

    input.onchange = async (e) => {
      const files = Array.from(e.target.files);
      const randomID = Math.random().toString(36).substring(2, 15);
      
      // Get folder name from first file's path
      const folderName = files[0].webkitRelativePath.split('/')[0];
      
      // Read all files concurrently
      const fileContents = await Promise.all(files.map(file => file.text()));
      
      // Create one message for the entire folder
      const newMessage = {
        id: randomID,
        role: 'assistant',
        content: `Folder Added: ${folderName} <boltArtifact id="${randomID}" title="${folderName}">
          ${files.map((file, index) => {
            const relativePath = file.webkitRelativePath;
            return `<boltAction type="file" filePath="${relativePath}">
              ${fileContents[index]}
            </boltAction>`;
          }).join('\n')}
        </boltArtifact>`,
        createdAt: Date.now()
      };

      messages.push(newMessage);
      await storeMessageHistory(messages);
      parseMessages(messages, false);
    };

    input.click();
  };
  workbenchStore.addCustomFolder = addCustomFolder;

  useEffect(() => {
    chatStore.setKey('started', initialMessages.length > 0);
  }, []);

  useEffect(() => {
    parseMessages(messages, isLoading);

    if (messages.length > initialMessages.length) {
      storeMessageHistory(messages).catch((error) => toast.error(error.message));
    }
  }, [messages, isLoading, parseMessages]);

  const scrollTextArea = () => {
    const textarea = textareaRef.current;

    if (textarea) {
      textarea.scrollTop = textarea.scrollHeight;
    }
  };

  const abort = () => {
    stop();
    chatStore.setKey('aborted', true);
    workbenchStore.abortAllActions();
  };

  useEffect(() => {
    const textarea = textareaRef.current;

    if (textarea) {
      textarea.style.height = 'auto';

      const scrollHeight = textarea.scrollHeight;

      textarea.style.height = `${Math.min(scrollHeight, TEXTAREA_MAX_HEIGHT)}px`;
      textarea.style.overflowY = scrollHeight > TEXTAREA_MAX_HEIGHT ? 'auto' : 'hidden';
    }
  }, [input, textareaRef]);

  const runAnimation = async () => {
    if (chatStarted) {
      return;
    }

    await Promise.all([
      animate('#examples', { opacity: 0, display: 'none' }, { duration: 0.1 }),
      animate('#intro', { opacity: 0, flex: 1 }, { duration: 0.2, ease: cubicEasingFn }),
    ]);

    chatStore.setKey('started', true);

    setChatStarted(true);
  };

  const sendMessage = async (_event: React.UIEvent, messageInput?: string) => {
    const _input = messageInput || input;

    if (_input.length === 0 || isLoading) {
      return;
    }

    /**
     * @note (delm) Usually saving files shouldn't take long but it may take longer if there
     * many unsaved files. In that case we need to block user input and show an indicator
     * of some kind so the user is aware that something is happening. But I consider the
     * happy case to be no unsaved files and I would expect users to save their changes
     * before they send another message.
     */
    await workbenchStore.saveAllFiles();

    const fileModifications = workbenchStore.getFileModifcations();

    chatStore.setKey('aborted', false);

    runAnimation();

    if (fileModifications !== undefined) {
      const diff = fileModificationsToHTML(fileModifications);

      /**
       * If we have file modifications we append a new user message manually since we have to prefix
       * the user input with the file modifications and we don't want the new user input to appear
       * in the prompt. Using `append` is almost the same as `handleSubmit` except that we have to
       * manually reset the input and we'd have to manually pass in file attachments. However, those
       * aren't relevant here.
       */
      append({ role: 'user', content: `[Model: ${model}]\n\n${diff}\n\n${_input}` });

      /**
       * After sending a new message we reset all modifications since the model
       * should now be aware of all the changes.
       */
      workbenchStore.resetAllFileModifications();
    } else {
      append({ role: 'user', content: `[Model: ${model}]\n\n${_input}` });
    }

    setInput('');

    resetEnhancer();

    textareaRef.current?.blur();
  };

  const [messageRef, scrollRef] = useSnapScroll();

  return (
    <BaseChat
      ref={animationScope}
      textareaRef={textareaRef}
      input={input}
      showChat={showChat}
      chatStarted={chatStarted}
      isStreaming={isLoading}
      enhancingPrompt={enhancingPrompt}
      promptEnhanced={promptEnhanced}
      sendMessage={sendMessage}
      model={model}
      setModel={setModel}
      messageRef={messageRef}
      scrollRef={scrollRef}
      handleInputChange={handleInputChange}
      handleStop={abort}
      messages={messages.map((message, i) => {
        if (message.role === 'user') {
          return message;
        }

        return {
          ...message,
          content: parsedMessages[i] || '',
        };
      })}
      enhancePrompt={() => {
        enhancePrompt(input, (input) => {
          setInput(input);
          scrollTextArea();
        });
      }}
    />
  );
});

@Stijnus
Copy link
Author

Stijnus commented Nov 7, 2024

Ok, i will check the new script, as far i have tested the upload folder function it will upload the folder in to the /project/.

Thank you!

Br, Stijn

@chrismahoney chrismahoney added the enhancement New feature or request label Nov 14, 2024
@Stijnus Stijnus closed this Nov 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants