Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion actions/me/scripts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export async function getScript(id: string): Promise<ParsedScript | undefined> {
}
}

export async function createScript(script: Script) {
export async function createScript(script: Script): Promise<Script> {
return await create(script, `scripts`);
}

Expand Down
18 changes: 18 additions & 0 deletions actions/threads.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,24 @@ export async function renameThread(id: string, name: string) {
);
}

export async function updateThreadScript(
threadId: string,
scriptId: string,
script: string
) {
const threadsDir = THREADS_DIR();
const threadPath = path.join(threadsDir, threadId);
const meta = await fs.readFile(path.join(threadPath, META_FILE), 'utf-8');
const threadMeta = JSON.parse(meta) as ThreadMeta;
threadMeta.scriptId = scriptId;
threadMeta.script = script;
threadMeta.updated = new Date();
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);
Expand Down
152 changes: 135 additions & 17 deletions components/scripts/script-save.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,44 @@ import {
Dropdown,
DropdownTrigger,
DropdownItem,
Modal,
ModalContent,
ModalBody,
ModalFooter,
ModalHeader,
Checkbox,
Spinner,
} from '@nextui-org/react';
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import { ChatContext } from '@/contexts/chat';
import { PiFloppyDiskThin, PiUser } from 'react-icons/pi';
import { updateScript } from '@/actions/me/scripts';
import { createScript, updateScript } from '@/actions/me/scripts';
import { stringify } from '@/actions/gptscript';
import { gatewayTool } from '@/actions/knowledge/util';
import { MessageType } from '@/components/chat/messages';
import { Input } from '@nextui-org/input';
import { updateThreadScript } from '@/actions/threads';

const SaveScriptDropdown = () => {
const {
selectedThreadId,
scriptId,
setScriptId,
setScript,
tools,
socket,
scriptContent,
setScriptContent,
setMessages,
} = useContext(ChatContext);

const [isOpen, setIsOpen] = useState(false);
const [newScriptName, setNewScriptName] = useState('');
const [newScriptPrivate, setNewScriptPrivate] = useState(true);
const [saving, setSaving] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
const [errorMessage, setErrorMessage] = useState('');

const knowledgeTool = gatewayTool();

function saveScript() {
Expand Down Expand Up @@ -59,23 +78,122 @@ const SaveScriptDropdown = () => {
});
}

async function saveScriptAs(newName: string) {
// The knowledge tool is dynamic and not controlled by the user. Don't add it to the saved tool.
const addedTools = tools.filter((t) => t !== knowledgeTool);

// find the root tool and then add the new tool
for (const block of scriptContent!) {
if (block.type === 'tool') {
block.tools = (block.tools || [])
.filter((t) => !addedTools.includes(t))
.concat(...addedTools);
block.name = newName;
break;
}
}

try {
const content = await stringify(scriptContent);
const slug =
newName.toLowerCase().replaceAll(' ', '-') +
'-' +
Math.random().toString(36).substring(2, 7);

const newScript = await createScript({
displayName: newName,
slug: slug,
content: content,
visibility: newScriptPrivate ? 'private' : 'public',
});

setScriptContent([...scriptContent]);
socket?.emit('saveScript');

await updateThreadScript(
selectedThreadId!,
'' + newScript.id,
newScript.publicURL || ''
);

setScriptId('' + newScript.id);
setScript(newScript.publicURL!);

setSuccessMessage(`New Assistant Saved As ${newScript.displayName}`);
await new Promise((resolve) => setTimeout(resolve, 3000));

setIsOpen(false);
} catch (e: any) {
console.error(e);
setErrorMessage(e.toString());
}

setSaving(false);
}

return (
<Dropdown placement="bottom-start">
<DropdownTrigger>
<Button variant="light" isIconOnly>
<PiFloppyDiskThin className="size-5" />
</Button>
</DropdownTrigger>
<DropdownMenu>
{scriptId ? (
<DropdownItem key="save" onPress={saveScript}>
Save Assistant
<>
<Dropdown placement="bottom-start">
<DropdownTrigger>
<Button variant="light" isIconOnly>
<PiFloppyDiskThin className="size-5" />
</Button>
</DropdownTrigger>
<DropdownMenu>
{scriptId ? (
<DropdownItem key="save" onPress={saveScript}>
Save Assistant
</DropdownItem>
) : (
(null as any)
)}
<DropdownItem key="save-as" onPress={() => setIsOpen(true)}>
Save Assistant As
</DropdownItem>
) : (
(null as any)
)}
</DropdownMenu>
</Dropdown>
</DropdownMenu>
</Dropdown>

<Modal backdrop="opaque" isOpen={isOpen} onOpenChange={setIsOpen}>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">
Name Your New Assistant
</ModalHeader>
<ModalBody>
<Input
aria-label="new name"
label="New Name"
value={newScriptName}
onChange={(e) => setNewScriptName(e.target.value)}
/>
<Checkbox
defaultSelected={newScriptPrivate}
onChange={(e) => setNewScriptPrivate(e.target.checked)}
>
Private
</Checkbox>
<p className="text-red-500">{errorMessage}</p>
<p className="text-green-500">{successMessage}</p>
</ModalBody>
<ModalFooter>
<Button
disabled={saving}
aria-label="save-as"
color="primary"
onPress={() => {
setSaving(true);
saveScriptAs(newScriptName);
}}
>
{saving ? (
<Spinner size="sm" className="text-center" color="white" />
) : (
'Save As'
)}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};

Expand Down