Skip to content

Commit

Permalink
Merge pull request #36 from tylerslaton/threads-workspace
Browse files Browse the repository at this point in the history
Threads workspace
  • Loading branch information
tylerslaton authored Jul 25, 2024
2 parents 2404c11 + 93c34e6 commit fe9e699
Show file tree
Hide file tree
Showing 14 changed files with 353 additions and 188 deletions.
21 changes: 18 additions & 3 deletions actions/threads.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use server"

import {THREADS_DIR} from "@/config/env";
import {THREADS_DIR, WORKSPACE_DIR} from "@/config/env";
import {gpt} from "@/config/env";
import fs from "fs/promises";
import path from 'path';
Expand All @@ -20,6 +20,7 @@ export type ThreadMeta = {
updated: Date;
id: string;
script: string;
workspace: string;
}

export async function init() {
Expand Down Expand Up @@ -67,7 +68,11 @@ export async function getThreads() {

export async function getThread(id: string) {
const threads = await getThreads();
return threads.find(thread => thread.meta.id === id);
const thread = threads.find(thread => thread.meta.id === id);
if (!thread) return null;
// falsy check for workspace to account for old threads that don't have a workspace
if (thread.meta.workspace == undefined) thread.meta.workspace = WORKSPACE_DIR();
return thread;
}

async function newThreadName(): Promise<string> {
Expand Down Expand Up @@ -96,8 +101,9 @@ export async function createThread(script: string, firstMessage?: string): Promi
description: '',
created: new Date(),
updated: new Date(),
workspace: WORKSPACE_DIR(),
id,
script
script,
}
const threadState = '';

Expand Down Expand Up @@ -129,3 +135,12 @@ export async function renameThread(id: string, name: string) {
threadMeta.name = name;
await fs.writeFile(path.join(threadPath, META_FILE), JSON.stringify(threadMeta));
}

export async function updateThreadWorkspace(id: string, workspace: string) {
const threadsDir = THREADS_DIR();
const threadPath = path.join(threadsDir,id);
const meta = await fs.readFile(path.join(threadPath, META_FILE), "utf-8");
const threadMeta = JSON.parse(meta) as ThreadMeta;
threadMeta.workspace = workspace;
await fs.writeFile(path.join(threadPath, META_FILE), JSON.stringify(threadMeta));
}
21 changes: 9 additions & 12 deletions actions/upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,16 @@

import fs from "node:fs/promises";
import path from "node:path";
import {revalidatePath} from "next/cache";
import {WORKSPACE_DIR} from '@/config/env';
import {Dirent} from 'fs';

export async function uploadFile(formData: FormData) {
const workspaceDir = WORKSPACE_DIR()
await fs.mkdir(workspaceDir, {recursive: true})
import { revalidatePath } from "next/cache";
import { Dirent } from 'fs';

export async function uploadFile(workspace: string, formData: FormData) {
const file = formData.get("file") as File;
await fs.mkdir(workspace, { recursive: true })

const arrayBuffer = await file.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer);
await fs.writeFile(path.join(workspaceDir, file.name), buffer);

await fs.writeFile(path.join(workspace,file.name), buffer);
revalidatePath("/");
}

Expand All @@ -27,10 +24,10 @@ export async function deleteFile(path: string) {
}
}

export async function lsWorkspaceFiles(): Promise<string> {
export async function lsFiles(dir: string): Promise<string> {
let files: Dirent[] = []
try {
const dirents = await fs.readdir(WORKSPACE_DIR(), {withFileTypes: true});
const dirents = await fs.readdir(dir, { withFileTypes: true });
files = dirents.filter((dirent: Dirent) => !dirent.isDirectory());
} catch (e) {
if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {
Expand All @@ -39,4 +36,4 @@ export async function lsWorkspaceFiles(): Promise<string> {
}

return JSON.stringify(files);
}
}
19 changes: 11 additions & 8 deletions app/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {useSearchParams} from "next/navigation";
import Script from "@/components/script";
import Configure from "@/components/edit/configure";
import {EditContextProvider} from "@/contexts/edit";
import { ScriptContextProvider } from "@/contexts/script";
import New from "@/components/edit/new";
import ScriptNav from "@/components/edit/scriptNav";

Expand All @@ -25,15 +26,17 @@ function EditFile() {

return (
<EditContextProvider file={file}>
<div className="w-full h-full grid grid-cols-2">
<div className="absolute left-6 top-6">
<ScriptNav/>
<ScriptContextProvider initialScript={file} initialThread="">
<div className="w-full h-full grid grid-cols-2">
<div className="absolute left-6 top-6">
<ScriptNav/>
</div>
<div className="h-full overflow-auto w-full border-r-2 dark:border-zinc-800 p-6">
<Configure file={file}/>
</div>
<Script messagesHeight='h-[93%]' className="p-6 overflow-auto" file={file}/>
</div>
<div className="h-full overflow-auto w-full border-r-2 dark:border-zinc-800 p-6">
<Configure file={file}/>
</div>
<Script messagesHeight='h-[93%]' className="p-6 overflow-auto" file={file}/>
</div>
</ScriptContextProvider>
</EditContextProvider>
);
}
Expand Down
35 changes: 19 additions & 16 deletions app/run/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Suspense, useState} from 'react';
import Script from "@/components/script";
import Threads from "@/components/threads";
import {Thread} from '@/actions/threads';
import {ScriptContextProvider} from "@/contexts/script";


function RunFile() {
Expand All @@ -14,26 +15,28 @@ function RunFile() {
const [selectedThreadId, setSelectedThreadId] = useState<string | null>(null);

return (
<div className="w-full h-full flex pb-10">
<Threads
setThread={setThread}
setScript={setFile}
setThreads={setThreads}
threads={threads}
selectedThreadId={selectedThreadId}
setSelectedThreadId={setSelectedThreadId}
/>
<div className="mx-auto w-1/2">
<Script
enableThreads
className="pb-10"
file={file}
thread={thread}
<ScriptContextProvider initialScript={file} initialThread={thread}>
<div className="w-full h-full flex pb-10">
<Threads
setThread={setThread}
setScript={setFile}
setThreads={setThreads}
threads={threads}
selectedThreadId={selectedThreadId}
setSelectedThreadId={setSelectedThreadId}
/>
<div className="mx-auto w-1/2">
<Script
enableThreads
className="pb-10"
file={file}
thread={thread}
setThreads={setThreads}
setSelectedThreadId={setSelectedThreadId}
/>
</div>
</div>
</div>
</ScriptContextProvider>
);
}

Expand Down
20 changes: 9 additions & 11 deletions components/edit/configure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import {
Button,
Accordion,
AccordionItem,
Autocomplete,
AutocompleteItem,

} from "@nextui-org/react";
import {getModels} from "@/actions/models";
import {FaPlus} from "react-icons/fa";
Expand All @@ -29,9 +26,11 @@ interface ConfigureProps {

const Configure: React.FC<ConfigureProps> = ({file, className, custom}) => {
const {
root, setRoot,
tools, setTools,
loading, setLoading,
root,
setRoot,
tools,
setTools,
loading,
newestToolName,
} = useContext(EditContext);
const [models, setModels] = useState<string[]>([]);
Expand Down Expand Up @@ -70,9 +69,9 @@ const Configure: React.FC<ConfigureProps> = ({file, className, custom}) => {
placement="bottom"
closeDelay={0.5}
>
<Avatar
size="md"
name={abbreviate(root.name || 'Main')}
<Avatar
size="md"
name={abbreviate(root.name || 'Main')}
className="mx-auto mb-6 mt-4"
classNames={{base: "bg-white p-6 text-sm border dark:border-none dark:bg-zinc-900"}}
/>
Expand Down Expand Up @@ -105,8 +104,7 @@ const Configure: React.FC<ConfigureProps> = ({file, className, custom}) => {
defaultValue={root.instructions}
onChange={(e) => setRoot({...root, instructions: e.target.value})}
/>
<Models options={models} defaultValue={root.modelName}
onChange={(model) => setRoot({...root, modelName: model})}/>
<Models options={models} defaultValue={root.modelName} onChange={(model) => setRoot({...root, modelName: model})} />
<Imports className="py-2" tools={root.tools} setTools={setRootTools} label={"Basic tool"}/>
<Imports className="py-2" tools={root.context} setTools={setRootContexts} label={"context Tool"}/>
<Imports className="py-2" tools={root.agents} setTools={setRootAgents} label={"agent Tool"}/>
Expand Down
91 changes: 28 additions & 63 deletions components/script.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
"use client"

import React, {useState, useEffect, useRef, useCallback} from "react";
import type {Tool} from "@gptscript-ai/gptscript";
import React, {useEffect, useContext, useCallback, useRef} from "react";
import Messages, {MessageType} from "@/components/script/messages";
import ChatBar from "@/components/script/chatBar";
import ToolForm from "@/components/script/form";
import Loading from "@/components/loading";
import useChatSocket from '@/components/script/useChatSocket';
import {Button} from "@nextui-org/react";
import {fetchScript, path} from "@/actions/scripts/fetch";
import {getWorkspaceDir} from "@/actions/workspace";
import {createThread, getThreads, generateThreadName, renameThread, Thread} from "@/actions/threads";
import debounce from "lodash/debounce";
import { ScriptContext } from "@/contexts/script";
import {fetchScript, path} from "@/actions/scripts/fetch";

interface ScriptProps {
file: string;
Expand All @@ -23,59 +21,38 @@ interface ScriptProps {
setSelectedThreadId?: React.Dispatch<React.SetStateAction<string | null>>
}

const Script: React.FC<ScriptProps> = ({file, thread, setThreads, className, messagesHeight = 'h-full', enableThreads, setSelectedThreadId}) => {
const [tool, setTool] = useState<Tool>({} as Tool);
const [showForm, setShowForm] = useState(true);
const [formValues, setFormValues] = useState<Record<string, string>>({});
const [inputValue, setInputValue] = useState('');
const Script: React.FC<ScriptProps> = ({className, messagesHeight = 'h-full', enableThreads}) => {
const inputRef = useRef<HTMLInputElement>(null);
const [hasRun, setHasRun] = useState(false);
const [hasParams, setHasParams] = useState(false);
const [isEmpty, setIsEmpty] = useState(false);
const [inputValue, setInputValue] = React.useState<string>("");
const {
socket, connected, running, messages, setMessages, restart, interrupt, generating, error
} = useChatSocket(isEmpty);

const fetchThreads = async () => {
if (!setThreads) return;
const threads = await getThreads();
setThreads(threads);
};

useEffect(() => {
setHasParams(tool.arguments?.properties != undefined && Object.keys(tool.arguments?.properties).length > 0);
setIsEmpty(!tool.instructions);
}, [tool]);

useEffect(() => {
if (thread) restartScript();
}, [thread]);

useEffect(() => {
if (hasRun || !socket || !connected) return;
if (!tool.arguments?.properties || Object.keys(tool.arguments.properties).length === 0) {
path(file)
.then(async (path) => {
const workspace = await getWorkspaceDir()
return {path, workspace}
})
.then(({path, workspace}) => {
socket.emit("run", path, tool.name, formValues, workspace, thread)
});
setHasRun(true);
}
}, [tool, connected, file, formValues, thread]);
script,
tool,
showForm,
setShowForm,
formValues,
setFormValues,
setHasRun,
hasParams,
messages,
setMessages,
thread,
setThreads,
setSelectedThreadId,
socket,
connected,
running,
generating,
restartScript,
interrupt,
fetchThreads,
} = useContext(ScriptContext);

useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, [messages, inputValue]);

useEffect(() => {
fetchScript(file).then((data) => setTool(data));
}, []);

useEffect(() => {
const smallBody = document.getElementById("small-message");
if (smallBody) smallBody.scrollTop = smallBody.scrollHeight;
Expand All @@ -84,7 +61,7 @@ const Script: React.FC<ScriptProps> = ({file, thread, setThreads, className, mes
const handleFormSubmit = () => {
setShowForm(false);
setMessages([]);
path(file)
path(script)
.then(async (path) => {
const workspace = await getWorkspaceDir()
return {path, workspace}
Expand All @@ -107,7 +84,7 @@ const Script: React.FC<ScriptProps> = ({file, thread, setThreads, className, mes

let threadId = "";
if (hasNoUserMessages() && enableThreads && !thread && setThreads && setSelectedThreadId) {
const newThread = await createThread(file, message)
const newThread = await createThread(script, message)
threadId = newThread?.meta?.id;
setThreads(await getThreads());
setSelectedThreadId(threadId);
Expand All @@ -124,18 +101,6 @@ const Script: React.FC<ScriptProps> = ({file, thread, setThreads, className, mes

};

const restartScript = useCallback(
// This is debonced as allowing the user to spam the restart button can cause race
// conditions. In particular, the restart may not be processed correctly and can
// get the user into a state where no run has been sent to the server.
debounce(async () => {
setTool(await fetchScript(file));
restart();
setHasRun(false);
}, 200),
[file, restart]
);

const hasNoUserMessages = useCallback(() => messages.filter((m) => m.type === MessageType.User).length === 0, [messages]);

return (
Expand Down
Loading

0 comments on commit fe9e699

Please sign in to comment.