Skip to content

Commit fe9e699

Browse files
authoredJul 25, 2024
Merge pull request #36 from tylerslaton/threads-workspace
Threads workspace
2 parents 2404c11 + 93c34e6 commit fe9e699

File tree

14 files changed

+353
-188
lines changed

14 files changed

+353
-188
lines changed
 

‎actions/threads.tsx

+18-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use server"
22

3-
import {THREADS_DIR} from "@/config/env";
3+
import {THREADS_DIR, WORKSPACE_DIR} from "@/config/env";
44
import {gpt} from "@/config/env";
55
import fs from "fs/promises";
66
import path from 'path';
@@ -20,6 +20,7 @@ export type ThreadMeta = {
2020
updated: Date;
2121
id: string;
2222
script: string;
23+
workspace: string;
2324
}
2425

2526
export async function init() {
@@ -67,7 +68,11 @@ export async function getThreads() {
6768

6869
export async function getThread(id: string) {
6970
const threads = await getThreads();
70-
return threads.find(thread => thread.meta.id === id);
71+
const thread = threads.find(thread => thread.meta.id === id);
72+
if (!thread) return null;
73+
// falsy check for workspace to account for old threads that don't have a workspace
74+
if (thread.meta.workspace == undefined) thread.meta.workspace = WORKSPACE_DIR();
75+
return thread;
7176
}
7277

7378
async function newThreadName(): Promise<string> {
@@ -96,8 +101,9 @@ export async function createThread(script: string, firstMessage?: string): Promi
96101
description: '',
97102
created: new Date(),
98103
updated: new Date(),
104+
workspace: WORKSPACE_DIR(),
99105
id,
100-
script
106+
script,
101107
}
102108
const threadState = '';
103109

@@ -129,3 +135,12 @@ export async function renameThread(id: string, name: string) {
129135
threadMeta.name = name;
130136
await fs.writeFile(path.join(threadPath, META_FILE), JSON.stringify(threadMeta));
131137
}
138+
139+
export async function updateThreadWorkspace(id: string, workspace: string) {
140+
const threadsDir = THREADS_DIR();
141+
const threadPath = path.join(threadsDir,id);
142+
const meta = await fs.readFile(path.join(threadPath, META_FILE), "utf-8");
143+
const threadMeta = JSON.parse(meta) as ThreadMeta;
144+
threadMeta.workspace = workspace;
145+
await fs.writeFile(path.join(threadPath, META_FILE), JSON.stringify(threadMeta));
146+
}

‎actions/upload.tsx

+9-12
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,16 @@
22

33
import fs from "node:fs/promises";
44
import path from "node:path";
5-
import {revalidatePath} from "next/cache";
6-
import {WORKSPACE_DIR} from '@/config/env';
7-
import {Dirent} from 'fs';
8-
9-
export async function uploadFile(formData: FormData) {
10-
const workspaceDir = WORKSPACE_DIR()
11-
await fs.mkdir(workspaceDir, {recursive: true})
5+
import { revalidatePath } from "next/cache";
6+
import { Dirent } from 'fs';
127

8+
export async function uploadFile(workspace: string, formData: FormData) {
139
const file = formData.get("file") as File;
10+
await fs.mkdir(workspace, { recursive: true })
11+
1412
const arrayBuffer = await file.arrayBuffer();
1513
const buffer = new Uint8Array(arrayBuffer);
16-
await fs.writeFile(path.join(workspaceDir, file.name), buffer);
17-
14+
await fs.writeFile(path.join(workspace,file.name), buffer);
1815
revalidatePath("/");
1916
}
2017

@@ -27,10 +24,10 @@ export async function deleteFile(path: string) {
2724
}
2825
}
2926

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

4138
return JSON.stringify(files);
42-
}
39+
}

‎app/edit/page.tsx

+11-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {useSearchParams} from "next/navigation";
55
import Script from "@/components/script";
66
import Configure from "@/components/edit/configure";
77
import {EditContextProvider} from "@/contexts/edit";
8+
import { ScriptContextProvider } from "@/contexts/script";
89
import New from "@/components/edit/new";
910
import ScriptNav from "@/components/edit/scriptNav";
1011

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

2627
return (
2728
<EditContextProvider file={file}>
28-
<div className="w-full h-full grid grid-cols-2">
29-
<div className="absolute left-6 top-6">
30-
<ScriptNav/>
29+
<ScriptContextProvider initialScript={file} initialThread="">
30+
<div className="w-full h-full grid grid-cols-2">
31+
<div className="absolute left-6 top-6">
32+
<ScriptNav/>
33+
</div>
34+
<div className="h-full overflow-auto w-full border-r-2 dark:border-zinc-800 p-6">
35+
<Configure file={file}/>
36+
</div>
37+
<Script messagesHeight='h-[93%]' className="p-6 overflow-auto" file={file}/>
3138
</div>
32-
<div className="h-full overflow-auto w-full border-r-2 dark:border-zinc-800 p-6">
33-
<Configure file={file}/>
34-
</div>
35-
<Script messagesHeight='h-[93%]' className="p-6 overflow-auto" file={file}/>
36-
</div>
39+
</ScriptContextProvider>
3740
</EditContextProvider>
3841
);
3942
}

‎app/run/page.tsx

+19-16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {Suspense, useState} from 'react';
55
import Script from "@/components/script";
66
import Threads from "@/components/threads";
77
import {Thread} from '@/actions/threads';
8+
import {ScriptContextProvider} from "@/contexts/script";
89

910

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

1617
return (
17-
<div className="w-full h-full flex pb-10">
18-
<Threads
19-
setThread={setThread}
20-
setScript={setFile}
21-
setThreads={setThreads}
22-
threads={threads}
23-
selectedThreadId={selectedThreadId}
24-
setSelectedThreadId={setSelectedThreadId}
25-
/>
26-
<div className="mx-auto w-1/2">
27-
<Script
28-
enableThreads
29-
className="pb-10"
30-
file={file}
31-
thread={thread}
18+
<ScriptContextProvider initialScript={file} initialThread={thread}>
19+
<div className="w-full h-full flex pb-10">
20+
<Threads
21+
setThread={setThread}
22+
setScript={setFile}
3223
setThreads={setThreads}
24+
threads={threads}
25+
selectedThreadId={selectedThreadId}
3326
setSelectedThreadId={setSelectedThreadId}
3427
/>
28+
<div className="mx-auto w-1/2">
29+
<Script
30+
enableThreads
31+
className="pb-10"
32+
file={file}
33+
thread={thread}
34+
setThreads={setThreads}
35+
setSelectedThreadId={setSelectedThreadId}
36+
/>
37+
</div>
3538
</div>
36-
</div>
39+
</ScriptContextProvider>
3740
);
3841
}
3942

‎components/edit/configure.tsx

+9-11
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ import {
1313
Button,
1414
Accordion,
1515
AccordionItem,
16-
Autocomplete,
17-
AutocompleteItem,
18-
1916
} from "@nextui-org/react";
2017
import {getModels} from "@/actions/models";
2118
import {FaPlus} from "react-icons/fa";
@@ -29,9 +26,11 @@ interface ConfigureProps {
2926

3027
const Configure: React.FC<ConfigureProps> = ({file, className, custom}) => {
3128
const {
32-
root, setRoot,
33-
tools, setTools,
34-
loading, setLoading,
29+
root,
30+
setRoot,
31+
tools,
32+
setTools,
33+
loading,
3534
newestToolName,
3635
} = useContext(EditContext);
3736
const [models, setModels] = useState<string[]>([]);
@@ -70,9 +69,9 @@ const Configure: React.FC<ConfigureProps> = ({file, className, custom}) => {
7069
placement="bottom"
7170
closeDelay={0.5}
7271
>
73-
<Avatar
74-
size="md"
75-
name={abbreviate(root.name || 'Main')}
72+
<Avatar
73+
size="md"
74+
name={abbreviate(root.name || 'Main')}
7675
className="mx-auto mb-6 mt-4"
7776
classNames={{base: "bg-white p-6 text-sm border dark:border-none dark:bg-zinc-900"}}
7877
/>
@@ -105,8 +104,7 @@ const Configure: React.FC<ConfigureProps> = ({file, className, custom}) => {
105104
defaultValue={root.instructions}
106105
onChange={(e) => setRoot({...root, instructions: e.target.value})}
107106
/>
108-
<Models options={models} defaultValue={root.modelName}
109-
onChange={(model) => setRoot({...root, modelName: model})}/>
107+
<Models options={models} defaultValue={root.modelName} onChange={(model) => setRoot({...root, modelName: model})} />
110108
<Imports className="py-2" tools={root.tools} setTools={setRootTools} label={"Basic tool"}/>
111109
<Imports className="py-2" tools={root.context} setTools={setRootContexts} label={"context Tool"}/>
112110
<Imports className="py-2" tools={root.agents} setTools={setRootAgents} label={"agent Tool"}/>

‎components/script.tsx

+28-63
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
"use client"
22

3-
import React, {useState, useEffect, useRef, useCallback} from "react";
4-
import type {Tool} from "@gptscript-ai/gptscript";
3+
import React, {useEffect, useContext, useCallback, useRef} from "react";
54
import Messages, {MessageType} from "@/components/script/messages";
65
import ChatBar from "@/components/script/chatBar";
76
import ToolForm from "@/components/script/form";
87
import Loading from "@/components/loading";
9-
import useChatSocket from '@/components/script/useChatSocket';
108
import {Button} from "@nextui-org/react";
11-
import {fetchScript, path} from "@/actions/scripts/fetch";
129
import {getWorkspaceDir} from "@/actions/workspace";
1310
import {createThread, getThreads, generateThreadName, renameThread, Thread} from "@/actions/threads";
14-
import debounce from "lodash/debounce";
11+
import { ScriptContext } from "@/contexts/script";
12+
import {fetchScript, path} from "@/actions/scripts/fetch";
1513

1614
interface ScriptProps {
1715
file: string;
@@ -23,59 +21,38 @@ interface ScriptProps {
2321
setSelectedThreadId?: React.Dispatch<React.SetStateAction<string | null>>
2422
}
2523

26-
const Script: React.FC<ScriptProps> = ({file, thread, setThreads, className, messagesHeight = 'h-full', enableThreads, setSelectedThreadId}) => {
27-
const [tool, setTool] = useState<Tool>({} as Tool);
28-
const [showForm, setShowForm] = useState(true);
29-
const [formValues, setFormValues] = useState<Record<string, string>>({});
30-
const [inputValue, setInputValue] = useState('');
24+
const Script: React.FC<ScriptProps> = ({className, messagesHeight = 'h-full', enableThreads}) => {
3125
const inputRef = useRef<HTMLInputElement>(null);
32-
const [hasRun, setHasRun] = useState(false);
33-
const [hasParams, setHasParams] = useState(false);
34-
const [isEmpty, setIsEmpty] = useState(false);
26+
const [inputValue, setInputValue] = React.useState<string>("");
3527
const {
36-
socket, connected, running, messages, setMessages, restart, interrupt, generating, error
37-
} = useChatSocket(isEmpty);
38-
39-
const fetchThreads = async () => {
40-
if (!setThreads) return;
41-
const threads = await getThreads();
42-
setThreads(threads);
43-
};
44-
45-
useEffect(() => {
46-
setHasParams(tool.arguments?.properties != undefined && Object.keys(tool.arguments?.properties).length > 0);
47-
setIsEmpty(!tool.instructions);
48-
}, [tool]);
49-
50-
useEffect(() => {
51-
if (thread) restartScript();
52-
}, [thread]);
53-
54-
useEffect(() => {
55-
if (hasRun || !socket || !connected) return;
56-
if (!tool.arguments?.properties || Object.keys(tool.arguments.properties).length === 0) {
57-
path(file)
58-
.then(async (path) => {
59-
const workspace = await getWorkspaceDir()
60-
return {path, workspace}
61-
})
62-
.then(({path, workspace}) => {
63-
socket.emit("run", path, tool.name, formValues, workspace, thread)
64-
});
65-
setHasRun(true);
66-
}
67-
}, [tool, connected, file, formValues, thread]);
28+
script,
29+
tool,
30+
showForm,
31+
setShowForm,
32+
formValues,
33+
setFormValues,
34+
setHasRun,
35+
hasParams,
36+
messages,
37+
setMessages,
38+
thread,
39+
setThreads,
40+
setSelectedThreadId,
41+
socket,
42+
connected,
43+
running,
44+
generating,
45+
restartScript,
46+
interrupt,
47+
fetchThreads,
48+
} = useContext(ScriptContext);
6849

6950
useEffect(() => {
7051
if (inputRef.current) {
7152
inputRef.current.focus();
7253
}
7354
}, [messages, inputValue]);
7455

75-
useEffect(() => {
76-
fetchScript(file).then((data) => setTool(data));
77-
}, []);
78-
7956
useEffect(() => {
8057
const smallBody = document.getElementById("small-message");
8158
if (smallBody) smallBody.scrollTop = smallBody.scrollHeight;
@@ -84,7 +61,7 @@ const Script: React.FC<ScriptProps> = ({file, thread, setThreads, className, mes
8461
const handleFormSubmit = () => {
8562
setShowForm(false);
8663
setMessages([]);
87-
path(file)
64+
path(script)
8865
.then(async (path) => {
8966
const workspace = await getWorkspaceDir()
9067
return {path, workspace}
@@ -107,7 +84,7 @@ const Script: React.FC<ScriptProps> = ({file, thread, setThreads, className, mes
10784

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

125102
};
126103

127-
const restartScript = useCallback(
128-
// This is debonced as allowing the user to spam the restart button can cause race
129-
// conditions. In particular, the restart may not be processed correctly and can
130-
// get the user into a state where no run has been sent to the server.
131-
debounce(async () => {
132-
setTool(await fetchScript(file));
133-
restart();
134-
setHasRun(false);
135-
}, 200),
136-
[file, restart]
137-
);
138-
139104
const hasNoUserMessages = useCallback(() => messages.filter((m) => m.type === MessageType.User).length === 0, [messages]);
140105

141106
return (

0 commit comments

Comments
 (0)