├── .vscode └── settings.json ├── assets └── chat.png ├── config ├── assistant.ts ├── fonts.ts ├── site.ts └── env.ts ├── electron ├── icon.icns ├── icon.ico ├── icon.png ├── entitlements.mac.plist ├── preload.mjs ├── config.mjs ├── build.mjs └── main.mjs ├── public ├── favicon.ico ├── vercel.svg ├── onedrive.svg ├── next.svg ├── notion.svg ├── squirrel.svg ├── lightmode_logo.svg └── darkmode_logo.svg ├── styles └── globals.css ├── tool.gpt ├── .eslintignore ├── .prettierignore ├── next.config.js ├── postcss.config.js ├── app ├── api │ ├── port │ │ └── route.ts │ ├── readyz │ │ └── route.ts │ └── logs │ │ └── route.ts ├── explore │ └── layout.tsx ├── settings │ ├── layout.tsx │ └── page.tsx ├── providers.tsx ├── layout.tsx ├── edit │ └── page.tsx ├── page.tsx ├── error.tsx └── build │ └── page.tsx ├── types ├── index.ts └── window.d.ts ├── actions ├── gateway.tsx ├── models.tsx ├── me │ ├── me.tsx │ └── scripts.tsx ├── workspace.tsx ├── scripts │ ├── new.tsx │ ├── update.tsx │ └── fetch.tsx ├── knowledge │ ├── util.ts │ ├── notion.ts │ ├── filehelper.ts │ ├── tool.ts │ ├── onedrive.ts │ └── knowledge.ts ├── appSettings.tsx ├── common.tsx ├── upload.tsx ├── gptscript.tsx ├── auth │ └── auth.tsx └── threads.tsx ├── hooks ├── useLogEffect.ts ├── useKeyEvent.ts ├── useDebounce.ts ├── useClickOutside.tsx └── useFetch.ts ├── .prettierrc ├── model └── knowledge.ts ├── .ackrc ├── components ├── assistant-not-found.tsx ├── shared │ └── tools │ │ ├── ToolActionChatMessage.tsx │ │ └── UrlToolModal.tsx ├── loading.tsx ├── navbar │ ├── me │ │ ├── logout.tsx │ │ └── login.tsx │ └── me.tsx ├── saveFile.tsx ├── scripts │ └── create.tsx ├── edit │ ├── configure │ │ ├── visibility.tsx │ │ ├── params.tsx │ │ └── models.tsx │ └── scriptNav.tsx ├── primitives.ts ├── chat │ ├── form.tsx │ ├── messages │ │ ├── promptForm.tsx │ │ ├── confirmForm.tsx │ │ └── calls.tsx │ └── chatBar │ │ ├── upload │ │ ├── files.tsx │ │ └── workspace.tsx │ │ ├── upload.tsx │ │ └── CatalogListBox.tsx ├── knowledge │ ├── KnowledgeModals.tsx │ ├── FileModal.tsx │ └── Notion.tsx ├── theme-switch.tsx ├── threads │ ├── menu.tsx │ └── new.tsx ├── threads.tsx └── navbar.tsx ├── .github └── workflows │ ├── pr.yml │ ├── dispatch.yml │ └── release.yml ├── .gitignore ├── contexts ├── nav.tsx ├── settings.tsx └── auth.tsx ├── .eslintrc.json ├── tailwind.config.js ├── tsconfig.json ├── LICENSE ├── server.mjs ├── package.json ├── scripts └── install-binary.mjs └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /assets/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptscript-ai/desktop/HEAD/assets/chat.png -------------------------------------------------------------------------------- /config/assistant.ts: -------------------------------------------------------------------------------- 1 | export const tildy = 'github.com/gptscript-ai/ui-assistant'; 2 | -------------------------------------------------------------------------------- /electron/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptscript-ai/desktop/HEAD/electron/icon.icns -------------------------------------------------------------------------------- /electron/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptscript-ai/desktop/HEAD/electron/icon.ico -------------------------------------------------------------------------------- /electron/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptscript-ai/desktop/HEAD/electron/icon.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gptscript-ai/desktop/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tool.gpt: -------------------------------------------------------------------------------- 1 | #!sys.daemon (path=/api/readyz) /usr/bin/env npm run --prefix ${GPTSCRIPT_TOOL_DIR} start 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | electron-dist/ 4 | .next 5 | tailwind.config.js 6 | .dockerigore 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | electron-dist/ 4 | .next 5 | tailwind.config.js 6 | .dockerigore 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /app/api/port/route.ts: -------------------------------------------------------------------------------- 1 | export async function POST(_req: Request) { 2 | return new Response(process.env.GPTSCRIPT_PORT ?? '3000'); 3 | } 4 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | import { SVGProps } from 'react'; 2 | 3 | export type IconSvgProps = SVGProps & { 4 | size?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /actions/gateway.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { GATEWAY_URL } from '@/config/env'; 4 | 5 | export const getGatewayUrl = async () => GATEWAY_URL(); 6 | -------------------------------------------------------------------------------- /hooks/useLogEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export function useLogEffect(...args: any[]) { 4 | useEffect(() => { 5 | console.log(...args); 6 | }, [...args]); 7 | } 8 | -------------------------------------------------------------------------------- /app/api/readyz/route.ts: -------------------------------------------------------------------------------- 1 | export async function GET(_req: Request) { 2 | return new Response('ok'); 3 | } 4 | 5 | export async function POST(_req: Request) { 6 | return new Response('ok'); 7 | } 8 | -------------------------------------------------------------------------------- /actions/models.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { gpt } from '@/config/env'; 4 | 5 | export const getModels = async (): Promise => { 6 | return (await gpt().listModels()).split('\n'); 7 | }; 8 | -------------------------------------------------------------------------------- /types/window.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | interface Window { 3 | electronAPI: { 4 | openFile: (path: string) => void; 5 | saveFile: (content: string) => void; 6 | }; 7 | } 8 | } 9 | 10 | export {}; 11 | -------------------------------------------------------------------------------- /actions/me/me.tsx: -------------------------------------------------------------------------------- 1 | import { get } from '@/actions/common'; 2 | 3 | export interface Me { 4 | username: string; 5 | email: string; 6 | } 7 | 8 | export async function getMe(): Promise { 9 | return await get('me', ''); 10 | } 11 | -------------------------------------------------------------------------------- /actions/workspace.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { WORKSPACE_DIR, set_WORKSPACE_DIR } from '@/config/env'; 4 | 5 | export const getWorkspaceDir = async () => WORKSPACE_DIR(); 6 | export const setWorkspaceDir = async (dir: string) => set_WORKSPACE_DIR(dir); 7 | -------------------------------------------------------------------------------- /config/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Fira_Code as FontMono, Inter as FontSans } from 'next/font/google'; 2 | 3 | export const fontSans = FontSans({ 4 | subsets: ['latin'], 5 | variable: '--font-sans', 6 | }); 7 | 8 | export const fontMono = FontMono({ 9 | subsets: ['latin'], 10 | variable: '--font-mono', 11 | }); 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "always", 11 | "endOfLine": "lf", 12 | "parser": "typescript" 13 | } 14 | -------------------------------------------------------------------------------- /model/knowledge.ts: -------------------------------------------------------------------------------- 1 | export const FileProviderType = { 2 | Local: 'local', 3 | OneDrive: 'onedrive', 4 | Notion: 'notion', 5 | } as const; 6 | 7 | export type FileProviderType = 8 | (typeof FileProviderType)[keyof typeof FileProviderType]; 9 | 10 | export interface FileDetail { 11 | fileName: string; 12 | size: number; 13 | type: FileProviderType; 14 | } 15 | -------------------------------------------------------------------------------- /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-dir=.git 2 | --ignore-dir=.nuxt 3 | --ignore-dir=.nuxt-prod 4 | --ignore-dir=.nyc_output 5 | --ignore-dir=.output 6 | --ignore-dir=.vscode 7 | --ignore-dir=coverage 8 | --ignore-dir=dist 9 | --ignore-dir=node_modules 10 | --ignore-dir=tmp 11 | --ignore-dir=vendor 12 | --ignore-file=ext:svg 13 | --ignore-file=is:selection.json 14 | --ignore-file=is:yarn.lock 15 | -------------------------------------------------------------------------------- /electron/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-unsigned-executable-memory 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/explore/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function ExploreLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 |
12 | {children} 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function EditLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 |
12 | {children} 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /components/assistant-not-found.tsx: -------------------------------------------------------------------------------- 1 | type NotFoundProps = { 2 | textSize?: string; 3 | spaceY?: string; 4 | }; 5 | 6 | export default function AssistantNotFound({ textSize, spaceY }: NotFoundProps) { 7 | return ( 8 |
11 |

12 | Assistant not found... 13 |

14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: '21' 20 | 21 | - name: Install dependencies 22 | run: npm install 23 | 24 | - name: Run linter 25 | run: npm run lint 26 | 27 | - name: Run build 28 | run: npm run build:electron 29 | -------------------------------------------------------------------------------- /electron/preload.mjs: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron'; 2 | 3 | // Import electron-log preload injection to enable log forwarding from clients (i.e. the renderer process) to the server 4 | // (i.e. the main process). 5 | import 'electron-log/preload.js'; 6 | 7 | contextBridge.exposeInMainWorld('electronAPI', { 8 | on: (channel, callback) => { 9 | ipcRenderer.on(channel, callback); 10 | }, 11 | send: (channel, args) => { 12 | ipcRenderer.send(channel, args); 13 | }, 14 | openFile: (file) => ipcRenderer.send('open-file', file), 15 | saveFile: (content) => ipcRenderer.send('save-file', content), 16 | }); 17 | -------------------------------------------------------------------------------- /.github/workflows/dispatch.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Tag Release of UI 3 | on: 4 | repository_dispatch: 5 | types: release 6 | 7 | jobs: 8 | tag-release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: Create a GitHub release 13 | uses: ncipollo/release-action@v1 14 | with: 15 | skipIfReleaseExists: true 16 | tag: ${{ github.event.client_payload.tag }} 17 | commit: main 18 | name: Release ${{ github.event.client_payload.tag }} 19 | generateReleaseNotes: true 20 | prerelease: ${{ contains(github.event.client_payload.tag, '-rc') }} -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | ; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | /electron-dist 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # dev env 35 | .idea/ 36 | 37 | # typescript 38 | *.tsbuildinfo 39 | next-env.d.ts 40 | 41 | # app output directories 42 | /gptscripts 43 | /threads 44 | 45 | bin/ 46 | -------------------------------------------------------------------------------- /components/shared/tools/ToolActionChatMessage.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Tooltip } from '@nextui-org/react'; 4 | 5 | export function ToolActionChatMessage({ 6 | action, 7 | name, 8 | toolRef, 9 | }: { 10 | action: 'Added' | 'Removed'; 11 | name?: string | null; 12 | toolRef: string; 13 | }) { 14 | return ( 15 |
16 | {action}{' '} 17 | {name ? ( 18 | 19 |

20 | {name} 21 |

22 |
23 | ) : ( 24 | toolRef 25 | )} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /contexts/nav.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState } from 'react'; 2 | 3 | interface NavContextProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | interface NavContextState { 8 | current: string; 9 | setCurrent: (current: string) => void; 10 | } 11 | 12 | const NavContext = createContext({} as NavContextState); 13 | const NavContextProvider: React.FC = ({ children }) => { 14 | const [current, setCurrent] = useState('/'); 15 | 16 | return ( 17 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export { NavContext, NavContextProvider }; 29 | -------------------------------------------------------------------------------- /components/loading.tsx: -------------------------------------------------------------------------------- 1 | import { AiOutlineLoading3Quarters } from 'react-icons/ai'; 2 | 3 | type LoadingProps = { 4 | children?: React.ReactNode; 5 | wheelSize?: string; 6 | textSize?: string; 7 | spaceY?: string; 8 | }; 9 | 10 | export default function Loading({ 11 | children, 12 | wheelSize, 13 | textSize, 14 | spaceY, 15 | }: LoadingProps) { 16 | return ( 17 |
20 | 23 |

{children}

24 |
25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/navbar/me/logout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useContext } from 'react'; 4 | import { logout } from '@/actions/auth/auth'; 5 | import { Button } from '@nextui-org/react'; 6 | import { AuthContext } from '@/contexts/auth'; 7 | import { GoPersonFill } from 'react-icons/go'; 8 | 9 | export default function Login() { 10 | const { setAuthenticated } = useContext(AuthContext); 11 | 12 | return ( 13 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:prettier/recommended" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": 2020, 11 | "sourceType": "module" 12 | }, 13 | "plugins": [ 14 | "prettier", 15 | "@typescript-eslint" 16 | ], 17 | "rules": { 18 | "@typescript-eslint/ban-ts-comment": "off", 19 | "prettier/prettier": ["error", { "endOfLine": "auto" }], 20 | "react/react-in-jsx-scope": "off", 21 | "react/prop-types": "off", 22 | "@typescript-eslint/no-explicit-any": "off", 23 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /components/saveFile.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Tooltip } from '@nextui-org/react'; 2 | import React from 'react'; 3 | import { GoDownload } from 'react-icons/go'; 4 | 5 | interface SaveFileProps { 6 | content: any; // any javascript object 7 | className?: string; 8 | } 9 | 10 | const SaveFile: React.FC = ({ content, className }) => { 11 | const handleSave = () => { 12 | window.electronAPI.saveFile(JSON.stringify(content, null, 2)); 13 | }; 14 | 15 | return ( 16 | 17 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "paths": { 27 | "@/*": [ 28 | "./*" 29 | ] 30 | } 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | "**/*.cjs", 37 | "**/*.js", 38 | "**/*.mjs", 39 | ".next/types/**/*.ts" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /config/env.ts: -------------------------------------------------------------------------------- 1 | import { GPTScript } from '@gptscript-ai/gptscript'; 2 | import path from 'path'; 3 | 4 | export const SCRIPTS_PATH = () => process.env.SCRIPTS_PATH || 'gptscripts'; 5 | export const WORKSPACE_DIR = () => process.env.WORKSPACE_DIR || ''; 6 | export const KNOWLEDGE_DIR = () => 7 | process.env.KNOWLEDGE_DIR || path.join(WORKSPACE_DIR(), 'knowledge'); 8 | export const THREADS_DIR = () => 9 | process.env.THREADS_DIR || path.join(WORKSPACE_DIR(), 'threads'); 10 | export const GATEWAY_URL = () => 11 | process.env.GPTSCRIPT_GATEWAY_URL || 'http://localhost:8080'; 12 | 13 | export const set_WORKSPACE_DIR = (dir: string) => 14 | (process.env.GPTSCRIPT_WORKSPACE_DIR = dir); 15 | 16 | let gptscript: GPTScript | null = null; 17 | 18 | export function gpt() { 19 | if (!gptscript) { 20 | gptscript = new GPTScript({ 21 | DefaultModelProvider: 'github.com/gptscript-ai/gateway-provider', 22 | }); 23 | } 24 | return gptscript; 25 | } 26 | -------------------------------------------------------------------------------- /hooks/useClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from 'react'; 2 | 3 | export function useClickOutside({ 4 | onClickOutside, 5 | action = 'click', 6 | whitelist, 7 | disable, 8 | }: { 9 | onClickOutside: (e: MouseEvent) => void; 10 | action?: 'click' | 'mousedown' | 'mouseup'; 11 | whitelist?: HTMLElement[]; 12 | disable?: boolean; 13 | }) { 14 | const ref = useRef(null); 15 | 16 | useEffect(() => { 17 | if (disable) return; 18 | 19 | const handleClickOutside = (e: MouseEvent) => { 20 | if ( 21 | ref.current && 22 | !ref.current.contains(e.target as Node) && 23 | !whitelist?.some((el) => el.contains(e.target as Node)) 24 | ) { 25 | onClickOutside(e); 26 | } 27 | }; 28 | 29 | document.addEventListener(action, handleClickOutside); 30 | 31 | return () => { 32 | document.removeEventListener(action, handleClickOutside); 33 | }; 34 | }, [action, onClickOutside, disable, whitelist]); 35 | 36 | return ref; 37 | } 38 | -------------------------------------------------------------------------------- /public/onedrive.svg: -------------------------------------------------------------------------------- 1 | OfficeCore10_32x_24x_20x_16x_01-22-2019 2 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { NextUIProvider } from '@nextui-org/system'; 5 | import { useRouter } from 'next/navigation'; 6 | import { ThemeProvider as NextThemesProvider } from 'next-themes'; 7 | import { ThemeProviderProps } from 'next-themes/dist/types'; 8 | import { AuthContextProvider } from '@/contexts/auth'; 9 | import { NavContextProvider } from '@/contexts/nav'; 10 | import { SettingsContextProvider } from '@/contexts/settings'; 11 | 12 | export interface ProvidersProps { 13 | children: React.ReactNode; 14 | themeProps?: ThemeProviderProps; 15 | } 16 | 17 | export function Providers({ children, themeProps }: ProvidersProps) { 18 | const router = useRouter(); 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Next UI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server.mjs: -------------------------------------------------------------------------------- 1 | import { startAppServer } from './server/app.mjs'; 2 | import open from 'open'; 3 | import dotenv from 'dotenv'; 4 | import { fileURLToPath } from 'url'; 5 | import { dirname } from 'path'; 6 | 7 | dotenv.config({ path: ['.env', '.env.local'] }); 8 | 9 | const dev = process.env.NODE_ENV !== 'production'; 10 | const hostname = 'localhost'; 11 | const port = parseInt(process.env.GPTSCRIPT_PORT ?? '3000'); 12 | const appDir = dirname(fileURLToPath(import.meta.url)); 13 | const runFile = process.env.UI_RUN_FILE; 14 | startAppServer({ dev, hostname, port, appDir }) 15 | .then((address) => { 16 | let landingPage = address; 17 | if (runFile) { 18 | landingPage = `${landingPage}/?file=${runFile}`; 19 | } 20 | 21 | open(landingPage) 22 | .then(() => { 23 | console.log(`${landingPage} opened!`); 24 | }) 25 | .catch((err) => { 26 | console.error( 27 | `Failed to open landing page ${landingPage}: ${err.message}` 28 | ); 29 | }); 30 | }) 31 | .catch((err) => { 32 | console.error(`Failed to start app server: ${err.message}`); 33 | process.exit(1); 34 | }); 35 | -------------------------------------------------------------------------------- /actions/knowledge/util.ts: -------------------------------------------------------------------------------- 1 | export function gatewayTool(): string { 2 | return 'github.com/gptscript-ai/knowledge/gateway@v0.4.14-rc.11'; 3 | } 4 | 5 | // This is a bit hacky because we need to make sure that the knowledge tool is updated to the latest version. 6 | // We do this by checking if prefix is github.com/gptscript-ai/knowledge. It will not work if tool is pointing to a fork so when devs are using forks be careful. 7 | export function ensureKnowledgeTool(tools: string[]): string[] { 8 | let found = false; 9 | for (let i = 0; i < tools.length; i++) { 10 | if (tools[i].startsWith('github.com/gptscript-ai/knowledge')) { 11 | tools[i] = gatewayTool(); 12 | found = true; 13 | break; 14 | } 15 | } 16 | 17 | if (!found) { 18 | tools.push(gatewayTool()); 19 | } 20 | return tools; 21 | } 22 | 23 | export function getCookie(name: string): string { 24 | const value = `; ${document.cookie}`; 25 | const parts = value.split(`; ${name}=`); 26 | if (parts.length === 2) { 27 | const value = parts?.pop()?.split(';').shift(); 28 | if (value) { 29 | return decodeURIComponent(value); 30 | } 31 | } 32 | return ''; 33 | } 34 | -------------------------------------------------------------------------------- /actions/knowledge/notion.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { WORKSPACE_DIR } from '@/config/env'; 6 | import { runSyncTool } from '@/actions/knowledge/tool'; 7 | 8 | export async function isNotionConfigured() { 9 | return fs.existsSync( 10 | path.join( 11 | WORKSPACE_DIR(), 12 | 'knowledge', 13 | 'integrations', 14 | 'notion', 15 | 'metadata.json' 16 | ) 17 | ); 18 | } 19 | 20 | export async function getNotionFiles() { 21 | const dir = path.join(WORKSPACE_DIR(), 'knowledge', 'integrations', 'notion'); 22 | const metadataFromFiles = fs.readFileSync(path.join(dir, 'metadata.json')); 23 | const metadata = JSON.parse(metadataFromFiles.toString()); 24 | const result = new Map(); 25 | for (const pageID in metadata) { 26 | const filePath = path.join(dir, pageID, metadata[pageID].filename); 27 | result.set(filePath, { 28 | url: metadata[pageID].url, 29 | fileName: path.basename(filePath), 30 | }); 31 | } 32 | 33 | return result; 34 | } 35 | 36 | export async function runNotionSync(authed: boolean): Promise { 37 | return runSyncTool(authed, 'notion'); 38 | } 39 | -------------------------------------------------------------------------------- /actions/scripts/update.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { Tool, GPTScript, Block } from '@gptscript-ai/gptscript'; 4 | import { SCRIPTS_PATH, gpt } from '@/config/env'; 5 | import fs from 'fs/promises'; 6 | 7 | const external = (file: string): boolean => { 8 | return ( 9 | file.startsWith('http') || 10 | file.startsWith('https') || 11 | file.startsWith('github.com') 12 | ); 13 | }; 14 | 15 | export const path = async (file: string): Promise => { 16 | if (!external(file)) return `${SCRIPTS_PATH()}/${file}.gpt`; 17 | return file; 18 | }; 19 | 20 | export const updateScript = async (file: string, script: Block[]) => { 21 | if (external(file)) throw new Error('cannot update external tools'); 22 | 23 | try { 24 | await fs.writeFile( 25 | `${SCRIPTS_PATH()}/${file}.gpt`, 26 | await gpt().stringify(script) 27 | ); 28 | } catch (e) { 29 | throw e; 30 | } 31 | }; 32 | 33 | export const updateTool = async ( 34 | file: string, 35 | name: string, 36 | script: Block[] 37 | ) => { 38 | if (external(file)) throw new Error('cannot update external tools'); 39 | 40 | try { 41 | return await gpt().stringify(script); 42 | } catch (e) { 43 | throw e; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /hooks/useFetch.ts: -------------------------------------------------------------------------------- 1 | import { noop } from 'lodash'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | 4 | // This is Temporary... Will replace with useSWR in after confirmation of adding libraries to the project 5 | 6 | export function useAsync( 7 | callback: (...args: TParams) => Promise 8 | ) { 9 | const [data, setData] = useState(null); 10 | const [pending, setPending] = useState(false); 11 | const [error, setError] = useState(null); 12 | 13 | const executeAsync = useCallback( 14 | async (...args: TParams) => { 15 | setPending(true); 16 | const promise = callback(...args); 17 | 18 | promise 19 | .then(setData) 20 | .catch(setError) 21 | .finally(() => setPending(false)); 22 | 23 | return promise; 24 | }, 25 | [callback] 26 | ); 27 | 28 | const execute = useCallback((...args: TParams) => { 29 | // Safe execution of the callback 30 | executeAsync(...args).catch(noop); 31 | }, []); 32 | 33 | const clear = useCallback(() => { 34 | setData(null); 35 | setError(null); 36 | setPending(false); 37 | }, []); 38 | 39 | useEffect(() => { 40 | if (!error) return; 41 | console.error(error); 42 | }, [error]); 43 | 44 | return { data, pending, error, execute, executeAsync, clear }; 45 | } 46 | -------------------------------------------------------------------------------- /actions/appSettings.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import fs from 'fs'; 4 | 5 | export type AppSettings = { 6 | confirmToolCalls: boolean; 7 | browser: BrowserAppSettings; 8 | }; 9 | 10 | export type BrowserAppSettings = { 11 | headless: boolean; 12 | useDefaultSession: boolean; 13 | }; 14 | 15 | const defaultAppSettings: AppSettings = { 16 | confirmToolCalls: false, 17 | browser: { 18 | headless: false, 19 | useDefaultSession: false, 20 | } as BrowserAppSettings, 21 | }; 22 | 23 | export async function getAppSettings() { 24 | if (!process.env.GPTSCRIPT_SETTINGS_FILE) { 25 | throw new Error('GPTSCRIPT_SETTINGS_FILE not set'); 26 | } 27 | 28 | const location = process.env.GPTSCRIPT_SETTINGS_FILE; 29 | if (fs.existsSync(location)) { 30 | const AppSettings = fs.readFileSync(location, 'utf-8'); 31 | try { 32 | return JSON.parse(AppSettings) as AppSettings; 33 | } catch { 34 | console.error('Malformed settings file, using default settings...'); 35 | } 36 | } 37 | return defaultAppSettings; 38 | } 39 | 40 | export async function setAppSettings(AppSettings: AppSettings) { 41 | if (!process.env.GPTSCRIPT_SETTINGS_FILE) { 42 | throw new Error('GPTSCRIPT_SETTINGS_FILE not set'); 43 | } 44 | 45 | const location = process.env.GPTSCRIPT_SETTINGS_FILE; 46 | fs.writeFileSync(location, JSON.stringify(AppSettings, null, 2)); 47 | } 48 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | ; 11 | -------------------------------------------------------------------------------- /components/edit/configure/visibility.tsx: -------------------------------------------------------------------------------- 1 | import { Switch, Tooltip } from '@nextui-org/react'; 2 | import { MdPublic, MdPublicOff } from 'react-icons/md'; 3 | 4 | interface VisibilityProps { 5 | visibility: 'public' | 'private' | 'protected'; 6 | setVisibility: React.Dispatch< 7 | React.SetStateAction<'public' | 'private' | 'protected'> 8 | >; 9 | className?: string; 10 | } 11 | 12 | const Visibility: React.FC = ({ 13 | visibility, 14 | setVisibility, 15 | className, 16 | }) => { 17 | return ( 18 | 24 |

{visibility === 'public' ? 'Public' : 'Private'}

25 |

26 | {visibility === 'public' 27 | ? 'Toggle to make visible to only you.' 28 | : 'Toggle to make visible to everyone.'} 29 |

30 | 31 | } 32 | > 33 |
34 | 38 | setVisibility(e.target.checked ? 'public' : 'private') 39 | } 40 | thumbIcon={visibility === 'public' ? : } 41 | /> 42 |
43 |
44 | ); 45 | }; 46 | 47 | export default Visibility; 48 | -------------------------------------------------------------------------------- /components/primitives.ts: -------------------------------------------------------------------------------- 1 | import { tv } from 'tailwind-variants'; 2 | 3 | export const title = tv({ 4 | base: 'tracking-tight inline font-semibold', 5 | variants: { 6 | color: { 7 | violet: 'from-[#FF1CF7] to-[#b249f8]', 8 | yellow: 'from-[#FF705B] to-[#FFB457]', 9 | blue: 'from-[#5EA2EF] to-[#0072F5]', 10 | cyan: 'from-[#00b7fa] to-[#01cfea]', 11 | green: 'from-[#6FEE8D] to-[#17c964]', 12 | pink: 'from-[#FF72E1] to-[#F54C7A]', 13 | foreground: 'dark:from-[#FFFFFF] dark:to-[#4B4B4B]', 14 | }, 15 | size: { 16 | sm: 'text-3xl lg:text-4xl', 17 | md: 'text-[2.3rem] lg:text-5xl leading-9', 18 | lg: 'text-4xl lg:text-6xl', 19 | }, 20 | fullWidth: { 21 | true: 'w-full block', 22 | }, 23 | }, 24 | defaultVariants: { 25 | size: 'md', 26 | }, 27 | compoundVariants: [ 28 | { 29 | color: [ 30 | 'violet', 31 | 'yellow', 32 | 'blue', 33 | 'cyan', 34 | 'green', 35 | 'pink', 36 | 'foreground', 37 | ], 38 | class: 'bg-clip-text text-transparent bg-gradient-to-b', 39 | }, 40 | ], 41 | }); 42 | 43 | export const subtitle = tv({ 44 | base: 'w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full', 45 | variants: { 46 | fullWidth: { 47 | true: '!w-full', 48 | }, 49 | }, 50 | defaultVariants: { 51 | fullWidth: true, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [ 17 | ubuntu-latest, 18 | macos-latest, 19 | macos-13, 20 | windows-latest 21 | ] 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Set up Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: '22.x' 31 | 32 | - name: Install dependencies 33 | run: npm install 34 | 35 | - name: Decode and save APPLE_API_KEY 36 | if: runner.os == 'macOS' 37 | run: echo "${{ secrets.ELECTRON_APPLE_API_KEY }}" | base64 --decode > /tmp/apikey.p8 38 | 39 | - name: Build Electron app 40 | env: 41 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | EP_GH_IGNORE_TIME: true 43 | EP_DRAFT: false 44 | APPLE_API_KEY: /tmp/apikey.p8 45 | APPLE_API_KEY_ID: ${{ secrets.ELECTRON_APPLE_API_KEY_ID }} 46 | APPLE_API_ISSUER: ${{ secrets.ELECTRON_APPLE_API_ISSUER }} 47 | CSC_LINK: ${{ secrets.ELECTRON_CSC_LINK }} 48 | CSC_KEY_PASSWORD: ${{ secrets.ELECTRON_CSC_KEY_PASSWORD }} 49 | run: | 50 | npm run build:electron 51 | -------------------------------------------------------------------------------- /actions/common.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { cookies } from 'next/headers'; 4 | import { GATEWAY_URL } from '@/config/env'; 5 | 6 | export async function list(path: string): Promise { 7 | return request(undefined, path, 'GET'); 8 | } 9 | 10 | export async function get(path: string, id: string): Promise { 11 | if (id !== '') { 12 | path = `${path}/${id}`; 13 | } 14 | return request(undefined, path, 'GET'); 15 | } 16 | 17 | export async function update(obj: T, path: string): Promise { 18 | return request(obj, path, 'PATCH'); 19 | } 20 | 21 | export async function create(obj: T, path: string): Promise { 22 | return request(obj, path, 'POST'); 23 | } 24 | 25 | export async function del(id: string, path: string): Promise { 26 | return request(undefined, `${path}/${id}`, 'DELETE'); 27 | } 28 | 29 | export async function request( 30 | obj: T, 31 | path: string, 32 | method: string 33 | ): Promise { 34 | const resp = await fetch(`${GATEWAY_URL()}/api/${path}`, { 35 | method: method, 36 | headers: { 37 | 'Content-type': 'application/json', 38 | Authorization: `Bearer ${(cookies().get('gateway_token') || {}).value || ''}`, 39 | }, 40 | body: obj ? JSON.stringify(obj) : undefined, 41 | }); 42 | 43 | const res = await resp.json(); 44 | if (resp.status < 200 || resp.status >= 400) { 45 | throw Error(`Unexpected status ${resp.status}: ${res.error}`); 46 | } 47 | 48 | return res; 49 | } 50 | -------------------------------------------------------------------------------- /components/chat/form.tsx: -------------------------------------------------------------------------------- 1 | // ToolForm.tsx 2 | import React from 'react'; 3 | import { Input, Divider } from '@nextui-org/react'; 4 | import type { Tool } from '@gptscript-ai/gptscript'; 5 | 6 | const ToolForm = ({ 7 | tool, 8 | formValues, 9 | handleInputChange, 10 | }: { 11 | tool: Tool; 12 | formValues: { [key: string]: string }; 13 | handleInputChange: (event: React.ChangeEvent) => void; 14 | }) => ( 15 |
16 |

17 | {`You're about to run`} {tool.name} 18 |

19 |

20 | {`Almost there! The script you're trying to run is requesting input from you 21 | first. Fill them out and then get started by clicking the button at the 22 | bottom of the page.`} 23 |

24 | 25 | {tool.arguments?.properties && 26 | Object.entries(tool.arguments.properties).map(([argName, arg]) => ( 27 | 40 | ))} 41 | 42 | ); 43 | 44 | export default ToolForm; 45 | -------------------------------------------------------------------------------- /contexts/settings.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppSettings, 3 | getAppSettings, 4 | setAppSettings, 5 | } from '@/actions/appSettings'; 6 | import { createContext, useEffect, useState } from 'react'; 7 | 8 | interface SettingsContextProps { 9 | children: React.ReactNode; 10 | } 11 | 12 | interface SettingsContextState { 13 | loading: boolean; 14 | settings: AppSettings; 15 | saveSettings: (settings: AppSettings) => Promise; 16 | } 17 | 18 | const SettingsContext = createContext( 19 | {} as SettingsContextState 20 | ); 21 | const SettingsContextProvider: React.FC = ({ 22 | children, 23 | }) => { 24 | const [loading, setLoading] = useState(true); 25 | const [settings, setSettings] = useState({ 26 | confirmToolCalls: false, 27 | browser: { 28 | headless: false, 29 | useDefaultSession: false, 30 | }, 31 | }); 32 | 33 | useEffect(() => { 34 | getAppSettings() 35 | .then((settings) => setSettings(settings)) 36 | .then(() => setLoading(false)); 37 | }, []); 38 | 39 | const saveSettings = async (settings: AppSettings) => { 40 | setAppSettings(settings).then(() => setSettings(settings)); // update the file 41 | }; 42 | 43 | return ( 44 | 51 | {children} 52 | 53 | ); 54 | }; 55 | 56 | export { SettingsContext, SettingsContextProvider }; 57 | -------------------------------------------------------------------------------- /public/notion.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css'; 2 | import { Metadata, Viewport } from 'next'; 3 | import { siteConfig } from '@/config/site'; 4 | import { fontSans } from '@/config/fonts'; 5 | import { Providers } from './providers'; 6 | import { Navbar } from '@/components/navbar'; 7 | import clsx from 'clsx'; 8 | 9 | export const metadata: Metadata = { 10 | title: { 11 | default: siteConfig.name, 12 | template: `%s - ${siteConfig.name}`, 13 | }, 14 | description: siteConfig.description, 15 | icons: { 16 | icon: '/favicon.ico', 17 | }, 18 | }; 19 | 20 | export const viewport: Viewport = { 21 | themeColor: [ 22 | { media: '(prefers-color-scheme: light)', color: 'white' }, 23 | { media: '(prefers-color-scheme: dark)', color: 'black' }, 24 | ], 25 | }; 26 | 27 | export default function RootLayout({ 28 | children, 29 | }: { 30 | children: React.ReactNode; 31 | }) { 32 | return ( 33 | 34 | 35 | 41 | 42 |
43 | 44 | 45 |
46 | {children} 47 |
48 |
49 |
50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /app/edit/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect, useState, Suspense, useContext } from 'react'; 4 | import { useSearchParams } from 'next/navigation'; 5 | import Configure from '@/components/edit/configure'; 6 | import { EditContextProvider } from '@/contexts/edit'; 7 | import { ChatContextProvider } from '@/contexts/chat'; 8 | import ScriptNav from '@/components/edit/scriptNav'; 9 | import { NavContext } from '@/contexts/nav'; 10 | 11 | function EditFile() { 12 | const [file, _setFile] = useState( 13 | useSearchParams().get('file') || '' 14 | ); 15 | const [scriptId] = useState(useSearchParams().get('id') || ''); 16 | const [collapsed, setCollapsed] = useState(false); 17 | 18 | const { setCurrent } = useContext(NavContext); 19 | useEffect(() => setCurrent('/build'), []); 20 | 21 | return ( 22 | 27 | 28 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | ); 39 | } 40 | 41 | export default function Edit() { 42 | return ( 43 | 44 | 45 | 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /contexts/auth.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useEffect } from 'react'; 2 | import { Me, getMe } from '@/actions/me/me'; 3 | import { loginThroughTool } from '@/actions/auth/auth'; 4 | 5 | interface AuthContextProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | interface AuthContextState { 10 | authenticated: boolean; 11 | setAuthenticated: (authenticated: boolean) => void; 12 | me: Me | null; 13 | setMe: React.Dispatch>; 14 | loading: boolean; 15 | } 16 | 17 | const AuthContext = createContext({} as AuthContextState); 18 | const AuthContextProvider: React.FC = ({ children }) => { 19 | const [authenticated, setAuthenticated] = useState(false); 20 | const [me, setMe] = useState(null); 21 | const [loading, setLoading] = useState(true); 22 | 23 | useEffect(() => { 24 | setAuthenticated(document && document?.cookie?.includes('gateway_token')); 25 | if (!document || !document.cookie?.includes('gateway_token')) { 26 | loginThroughTool().then(() => { 27 | setLoading(false); 28 | }); 29 | } else { 30 | setLoading(false); 31 | } 32 | }, []); 33 | 34 | useEffect(() => { 35 | if (loading) setMe(null); 36 | else if (!me) 37 | getMe().then((me) => { 38 | setMe(me); 39 | setAuthenticated(!!me); 40 | }); 41 | }, [me, loading]); 42 | 43 | return ( 44 | 53 | {children} 54 | 55 | ); 56 | }; 57 | 58 | export { AuthContext, AuthContextProvider }; 59 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSearchParams } from 'next/navigation'; 4 | import React, { Suspense, useContext, useEffect, useState } from 'react'; 5 | import Chat from '@/components/chat'; 6 | import Threads from '@/components/threads'; 7 | import { ChatContextProvider } from '@/contexts/chat'; 8 | import { NavContext } from '@/contexts/nav'; 9 | import ExploreModal from '@/components/explore/ExploreModal'; 10 | import { useDisclosure } from '@nextui-org/react'; 11 | 12 | function RunFile() { 13 | const [script, _setScript] = useState( 14 | useSearchParams().get('file') ?? '' 15 | ); 16 | const [scriptId, _scriptId] = useState( 17 | useSearchParams().get('id') ?? '' 18 | ); 19 | const { isOpen, onOpen, onClose } = useDisclosure(); 20 | 21 | const { setCurrent } = useContext(NavContext); 22 | 23 | useEffect(() => setCurrent('/'), []); 24 | 25 | return ( 26 | 31 |
32 | 33 | 34 |
35 | 42 |
43 |
44 | 45 |
46 | ); 47 | } 48 | 49 | export default function Run() { 50 | return ( 51 | 52 | 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /components/knowledge/KnowledgeModals.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalBody, 4 | ModalContent, 5 | ModalHeader, 6 | Slider, 7 | } from '@nextui-org/react'; 8 | import { useContext } from 'react'; 9 | import { EditContext } from '@/contexts/edit'; 10 | 11 | interface KnowledgeProps { 12 | isFileSettingOpen: boolean; 13 | onFileSettingClose: () => void; 14 | } 15 | 16 | const KnowledgeModals = ({ 17 | isFileSettingOpen, 18 | onFileSettingClose, 19 | }: KnowledgeProps) => { 20 | const { topK, setTopK } = useContext(EditContext); 21 | return ( 22 | <> 23 | 29 | 30 | Settings 31 | 32 |
33 |

{`Number of Results:`}

34 |

{topK}

35 |
36 | { 43 | setTopK(value as number); 44 | }} 45 | aria-label="top-k" 46 | /> 47 |

48 | Select the number of most relevant documents from a retrieved set, 49 | based on their relevance scores 50 |

51 |
52 |
53 |
54 | 55 | ); 56 | }; 57 | 58 | export default KnowledgeModals; 59 | -------------------------------------------------------------------------------- /actions/knowledge/filehelper.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { WORKSPACE_DIR } from '@/config/env'; 6 | import { FileDetail } from '@/model/knowledge'; 7 | 8 | export async function getFileOrFolderSizeInKB( 9 | filePath: string 10 | ): Promise { 11 | const stats = fs.statSync(filePath); 12 | 13 | if (stats.isFile()) { 14 | // Convert file size from bytes to KB 15 | return parseFloat((stats.size / 1024).toFixed(2)); 16 | } 17 | 18 | if (stats.isDirectory()) { 19 | const files = fs.readdirSync(filePath); 20 | const folderSize = await Promise.all( 21 | files.map(async (file) => { 22 | const currentPath = path.join(filePath, file); 23 | return await getFileOrFolderSizeInKB(currentPath); 24 | }) 25 | ).then((sizes) => sizes.reduce((total, size) => total + size, 0)); 26 | 27 | return parseFloat(folderSize.toFixed(2)); 28 | } 29 | 30 | return 0; 31 | } 32 | 33 | export async function importFiles(files: string[]) { 34 | const result: Map = new Map(); 35 | 36 | for (const file of files) { 37 | // check if filepath lives in notion or onedrive integration folders 38 | // The file should live in a folder with the pattern ${WORKSPACE_DIR}/knowledge/integrations/${type}/${DocumentId}/${fileName} 39 | const baseDir = path.dirname(path.dirname(file)); 40 | let type = path.basename(baseDir); 41 | if ( 42 | type !== 'notion' && 43 | type !== 'onedrive' && 44 | baseDir !== path.join(WORKSPACE_DIR(), 'knowledge', 'integrations', type) 45 | ) { 46 | type = 'local'; 47 | } 48 | result.set(file, { 49 | fileName: path.basename(file), 50 | size: await getFileOrFolderSizeInKB(file), 51 | type: type as any, 52 | }); 53 | } 54 | 55 | return result; 56 | } 57 | -------------------------------------------------------------------------------- /actions/upload.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import fs from 'node:fs/promises'; 4 | import path from 'node:path'; 5 | import { revalidatePath } from 'next/cache'; 6 | import { Dirent } from 'fs'; 7 | 8 | export async function uploadFile( 9 | workspace: string, 10 | formData: FormData, 11 | isKnowledge?: boolean 12 | ) { 13 | if (isKnowledge) { 14 | workspace = path.join(path.dirname(workspace), 'knowledge'); 15 | } 16 | const file = formData.get('file') as File; 17 | await fs.mkdir(workspace, { recursive: true }); 18 | 19 | const arrayBuffer = await file.arrayBuffer(); 20 | const buffer = new Uint8Array(arrayBuffer); 21 | await fs.writeFile(path.join(workspace, file.name), buffer); 22 | revalidatePath('/'); 23 | } 24 | 25 | export async function deleteFile(path: string) { 26 | try { 27 | await fs.unlink(path); 28 | revalidatePath('/'); 29 | } catch (error) { 30 | console.error('Error deleting file:', error); 31 | } 32 | } 33 | 34 | export async function lsKnowledgeFiles(workspace: string): Promise { 35 | return lsFiles(path.join(path.dirname(workspace), 'knowledge')); 36 | } 37 | 38 | export async function deleteKnowledgeFile(workspace: string, name: string) { 39 | return deleteFile(path.join(path.dirname(workspace), 'knowledge', name)); 40 | } 41 | 42 | export async function clearThreadKnowledge(workspace: string) { 43 | return fs.rm(path.join(path.dirname(workspace), 'knowledge'), { 44 | recursive: true, 45 | force: true, 46 | }); 47 | } 48 | 49 | export async function lsFiles(dir: string): Promise { 50 | let files: Dirent[] = []; 51 | try { 52 | const dirents = await fs.readdir(dir, { withFileTypes: true }); 53 | files = dirents.filter((dirent: Dirent) => !dirent.isDirectory()); 54 | } catch (e) { 55 | if ((e as NodeJS.ErrnoException).code !== 'ENOENT') { 56 | throw e; 57 | } 58 | } 59 | 60 | return JSON.stringify(files); 61 | } 62 | -------------------------------------------------------------------------------- /app/api/logs/route.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs/promises'; 3 | import { NextResponse } from 'next/server'; 4 | import archiver from 'archiver'; 5 | import { PassThrough } from 'stream'; 6 | 7 | export async function GET(_req: Request) { 8 | const logDir = process.env.LOGS_DIR; 9 | 10 | console.log(`logsDir: ${logDir}`); 11 | 12 | if (!logDir) { 13 | return new NextResponse('LOGS_DIR environment variable is not set', { 14 | status: 500, 15 | }); 16 | } 17 | 18 | try { 19 | const logFiles = await fs.readdir(logDir); 20 | const zipFileName = 'logs.zip'; 21 | 22 | // Create a PassThrough stream to pipe the archive data 23 | const passThrough = new PassThrough(); 24 | 25 | // Create an archive instance 26 | const archive = archiver('zip', { zlib: { level: 9 } }); 27 | 28 | // Pipe archive data to the PassThrough stream 29 | archive.pipe(passThrough); 30 | 31 | logFiles.forEach((file) => { 32 | const filePath = path.join(logDir, file); 33 | archive.file(filePath, { name: file }); 34 | }); 35 | 36 | // Finalize the archive 37 | await archive.finalize(); 38 | 39 | // Convert Node.js stream to a Web ReadableStream 40 | const stream = new ReadableStream({ 41 | async start(controller) { 42 | passThrough.on('data', (chunk) => { 43 | controller.enqueue(chunk); 44 | }); 45 | 46 | passThrough.on('end', () => { 47 | controller.close(); 48 | }); 49 | 50 | passThrough.on('error', (err) => { 51 | controller.error(err); 52 | }); 53 | }, 54 | }); 55 | 56 | return new NextResponse(stream, { 57 | headers: { 58 | 'Content-Type': 'application/zip', 59 | 'Content-Disposition': `attachment; filename="${zipFileName}"`, 60 | }, 61 | }); 62 | } catch (error) { 63 | console.error('Error generating log ZIP:', error); 64 | return new NextResponse('Error generating log ZIP', { status: 500 }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /components/chat/messages/promptForm.tsx: -------------------------------------------------------------------------------- 1 | import { useForm } from 'react-hook-form'; 2 | import { GoCheckCircle } from 'react-icons/go'; 3 | import type { PromptFrame, PromptResponse } from '@gptscript-ai/gptscript'; 4 | import { Input, Button } from '@nextui-org/react'; 5 | 6 | const PromptForm = ({ 7 | frame, 8 | onSubmit, 9 | }: { 10 | frame: PromptFrame; 11 | onSubmit: (data: PromptResponse) => void; 12 | }) => { 13 | const { register, handleSubmit, getValues } = 14 | useForm>(); 15 | const noFields = !frame.fields || frame.fields.length === 0; 16 | if (noFields) { 17 | frame.fields = []; 18 | } 19 | 20 | const onSubmitForm = () => { 21 | onSubmit({ id: frame.id, responses: getValues() }); 22 | if (frame.metadata && frame.metadata.authURL) { 23 | open(frame.metadata.authURL); 24 | } 25 | }; 26 | 27 | let buttonText = noFields ? 'OK' : 'Submit'; 28 | let includeHiddenInput = false; 29 | if (noFields && frame.metadata && frame.metadata.authURL) { 30 | buttonText = 'Click here to sign in'; 31 | includeHiddenInput = true; 32 | } 33 | 34 | return ( 35 |
36 | {frame.fields.map( 37 | (field, index) => 38 | field && ( 39 | 47 | ) 48 | )} 49 | {includeHiddenInput && ( 50 | 56 | )} 57 | 66 |
67 | ); 68 | }; 69 | 70 | export default PromptForm; 71 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useEffect } from 'react'; 4 | import { useRouter } from 'next/navigation'; 5 | import { 6 | Modal, 7 | ModalContent, 8 | ModalHeader, 9 | ModalBody, 10 | ModalFooter, 11 | Button, 12 | } from '@nextui-org/react'; 13 | import { GoDownload } from 'react-icons/go'; 14 | import { LuRefreshCcw } from 'react-icons/lu'; 15 | 16 | export default function Error({ 17 | error, 18 | reset, 19 | }: { 20 | error: Error & { digest?: string }; 21 | reset: () => void; 22 | }) { 23 | const router = useRouter(); 24 | 25 | useEffect(() => { 26 | console.error(error); 27 | }, [error]); 28 | 29 | const handleClose = () => { 30 | router.push('/'); // Navigate to a safe page (home) 31 | }; 32 | 33 | return ( 34 | 46 | 47 | 48 |

Something went wrong!

49 |
50 | 51 | {error.message || 'An unexpected error occurred'} 52 | {error.digest && ( 53 | {`For more info, search logs for ${error.digest}`} 54 | )} 55 | 56 | 57 | 67 | 76 | 77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /components/shared/tools/UrlToolModal.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@nextui-org/button'; 4 | import { 5 | Modal, 6 | ModalContent, 7 | Input, 8 | ModalHeader, 9 | ModalBody, 10 | Spinner, 11 | Link, 12 | } from '@nextui-org/react'; 13 | import { useEffect, useState } from 'react'; 14 | 15 | export function UrlToolModal({ 16 | isOpen, 17 | onAddTool, 18 | onClose, 19 | error, 20 | isLoading, 21 | }: { 22 | isOpen: boolean; 23 | onClose: () => void; 24 | onAddTool: (tool: string) => void; 25 | error?: string; 26 | isLoading?: boolean; 27 | }) { 28 | const [toolUrl, setToolUrl] = useState(''); 29 | 30 | useEffect(() => { 31 | if (!isOpen) return; 32 | setToolUrl(''); 33 | }, [isOpen]); 34 | 35 | return ( 36 | 37 | 38 | Add Tool from URL 39 | 40 | 41 |
{ 44 | e.preventDefault(); 45 | onAddTool(toolUrl); 46 | setToolUrl(''); 47 | }} 48 | > 49 | setToolUrl(e.target.value)} 58 | placeholder="example: github.com/gptscript-ai/vision" 59 | /> 60 | 70 |
71 | 72 |

73 | Find more tools in our{' '} 74 | 75 | Community Catalog 76 | 77 |

78 |
79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /actions/knowledge/tool.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { 4 | GPTScript, 5 | PromptFrame, 6 | Run, 7 | RunEventType, 8 | } from '@gptscript-ai/gptscript'; 9 | import path from 'path'; 10 | import { WORKSPACE_DIR } from '@/config/env'; 11 | import fs from 'fs'; 12 | 13 | export async function runSyncTool( 14 | authed: boolean, 15 | tool: 'notion' | 'onedrive' 16 | ): Promise { 17 | const gptscript = new GPTScript({ 18 | DefaultModelProvider: 'github.com/gptscript-ai/gateway-provider', 19 | }); 20 | 21 | let toolUrl = ''; 22 | if (tool === 'notion') { 23 | toolUrl = 'github.com/gptscript-ai/knowledge-notion-integration'; 24 | } else if (tool === 'onedrive') { 25 | toolUrl = 'github.com/gptscript-ai/knowledge-onedrive-integration'; 26 | } 27 | const runningTool = await gptscript.run(toolUrl, { 28 | prompt: true, 29 | }); 30 | if (!authed) { 31 | const handlePromptEvent = (runningTool: Run) => { 32 | return new Promise((resolve) => { 33 | runningTool.on(RunEventType.Prompt, (data: PromptFrame) => { 34 | resolve(data.id); 35 | }); 36 | }); 37 | }; 38 | 39 | const id = await handlePromptEvent(runningTool); 40 | await gptscript.promptResponse({ id, responses: {} }); 41 | } 42 | await runningTool.text(); 43 | return; 44 | } 45 | 46 | /** 47 | * syncFiles syncs all files only when they are selected 48 | * todo: we can stop syncing once file is no longer used by any other script 49 | */ 50 | export async function syncFiles( 51 | selectedFiles: string[], 52 | type: 'notion' | 'onedrive' 53 | ): Promise { 54 | const dir = path.join(WORKSPACE_DIR(), 'knowledge', 'integrations', type); 55 | const metadataFromFiles = fs.readFileSync(path.join(dir, 'metadata.json')); 56 | const metadata = JSON.parse(metadataFromFiles.toString()); 57 | for (const file of selectedFiles) { 58 | const baseDir = path.dirname(path.dirname(file)); 59 | if (baseDir === dir) { 60 | const documentID = path.basename(path.dirname(file)); 61 | const detail = metadata[documentID]; 62 | detail.sync = true; 63 | metadata[documentID] = detail; 64 | } 65 | } 66 | fs.writeFileSync(path.join(dir, 'metadata.json'), JSON.stringify(metadata)); 67 | await runSyncTool(true, type); 68 | return; 69 | } 70 | -------------------------------------------------------------------------------- /actions/knowledge/onedrive.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import { WORKSPACE_DIR } from '@/config/env'; 5 | import { runSyncTool } from '@/actions/knowledge/tool'; 6 | 7 | export async function isOneDriveConfigured() { 8 | return fs.existsSync( 9 | path.join( 10 | WORKSPACE_DIR(), 11 | 'knowledge', 12 | 'integrations', 13 | 'onedrive', 14 | 'metadata.json' 15 | ) 16 | ); 17 | } 18 | 19 | export async function getOneDriveFiles(): Promise< 20 | Map 21 | > { 22 | const dir = path.join( 23 | WORKSPACE_DIR(), 24 | 'knowledge', 25 | 'integrations', 26 | 'onedrive' 27 | ); 28 | const metadataFromFile = fs.readFileSync(path.join(dir, 'metadata.json')); 29 | const metadata = JSON.parse(metadataFromFile.toString()); 30 | const result = new Map< 31 | string, 32 | { url: string; fileName: string; displayName: string } 33 | >(); 34 | for (const documentID in metadata) { 35 | result.set(path.join(dir, documentID, metadata[documentID].fileName), { 36 | url: metadata[documentID].url, 37 | fileName: metadata[documentID].fileName, 38 | displayName: metadata[documentID].displayName, 39 | }); 40 | } 41 | return result; 42 | } 43 | 44 | export async function syncSharedLink(link: string): Promise { 45 | const dir = path.join( 46 | WORKSPACE_DIR(), 47 | 'knowledge', 48 | 'integrations', 49 | 'onedrive' 50 | ); 51 | const externalLinkFile = path.join(dir, 'externalLinks.json'); 52 | if (!fs.existsSync(externalLinkFile)) { 53 | fs.writeFileSync(externalLinkFile, '{}'); 54 | } 55 | 56 | const externalLink = JSON.parse(fs.readFileSync(externalLinkFile).toString()); 57 | externalLink[link] = 'true'; 58 | fs.writeFileSync(externalLinkFile, JSON.stringify(externalLink)); 59 | 60 | await runSyncTool(true, 'onedrive'); 61 | return; 62 | } 63 | 64 | export async function clearOneDriveFiles(): Promise { 65 | const dir = path.join( 66 | WORKSPACE_DIR(), 67 | 'knowledge', 68 | 'integrations', 69 | 'onedrive' 70 | ); 71 | const externalLinkFile = path.join(dir, 'externalLinks.json'); 72 | fs.rmSync(externalLinkFile, { recursive: true, force: true }); 73 | } 74 | 75 | export async function runOneDriveSync(authed: boolean): Promise { 76 | return runSyncTool(authed, 'onedrive'); 77 | } 78 | -------------------------------------------------------------------------------- /components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { FC } from 'react'; 4 | import { VisuallyHidden } from '@react-aria/visually-hidden'; 5 | import { SwitchProps, useSwitch } from '@nextui-org/switch'; 6 | import { useTheme } from 'next-themes'; 7 | import { useIsSSR } from '@react-aria/ssr'; 8 | import clsx from 'clsx'; 9 | 10 | import { SunFilledIcon, MoonFilledIcon } from '@/components/icons'; 11 | 12 | export interface ThemeSwitchProps { 13 | className?: string; 14 | classNames?: SwitchProps['classNames']; 15 | } 16 | 17 | export const ThemeSwitch: FC = ({ 18 | className, 19 | classNames, 20 | }) => { 21 | const { theme, setTheme } = useTheme(); 22 | const isSSR = useIsSSR(); 23 | 24 | const onChange = () => { 25 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 26 | theme === 'light' ? setTheme('dark') : setTheme('light'); 27 | }; 28 | 29 | const { 30 | Component, 31 | slots, 32 | isSelected, 33 | getBaseProps, 34 | getInputProps, 35 | getWrapperProps, 36 | } = useSwitch({ 37 | isSelected: theme === 'light' || isSSR, 38 | 'aria-label': `Switch to ${theme === 'light' || isSSR ? 'dark' : 'light'} mode`, 39 | onChange, 40 | }); 41 | 42 | return ( 43 | 52 | 53 | 54 | 55 |
74 | {!isSelected || isSSR ? ( 75 | 76 | ) : ( 77 | 78 | )} 79 |
80 |
81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /actions/scripts/fetch.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { Tool, Block } from '@gptscript-ai/gptscript'; 4 | import { SCRIPTS_PATH, gpt } from '@/config/env'; 5 | import fs from 'fs/promises'; 6 | 7 | const external = (file: string): boolean => { 8 | return ( 9 | file.startsWith('http') || 10 | file.startsWith('https') || 11 | file.startsWith('github.com') 12 | ); 13 | }; 14 | 15 | export const path = async (file: string): Promise => { 16 | if (!external(file)) return `${SCRIPTS_PATH()}/${file}.gpt`; 17 | return file; 18 | }; 19 | 20 | export const fetchFullScript = async (file: string): Promise => { 21 | if (!external(file)) file = `${SCRIPTS_PATH()}/${file}.gpt`; 22 | 23 | return await gpt().parse(file); 24 | }; 25 | 26 | export const fetchScript = async (file: string): Promise => { 27 | if (!external(file)) file = `${SCRIPTS_PATH()}/${file}.gpt`; 28 | 29 | try { 30 | const script = await gpt().parse(file); 31 | for (const tool of script) { 32 | if (tool.type === 'text') continue; 33 | return tool; 34 | } 35 | return {} as Tool; 36 | } catch (e) { 37 | if (`${e}`.includes('no such file')) { 38 | return {} as Tool; 39 | } 40 | throw e; 41 | } 42 | }; 43 | 44 | export const fetchScripts = async (): Promise> => { 45 | try { 46 | const files = await fs.readdir(SCRIPTS_PATH()); 47 | const gptFiles = files.filter((file) => file.endsWith('.gpt')); 48 | 49 | if (gptFiles.length === 0) return {} as Record; 50 | 51 | const scripts: Record = {}; 52 | for (const file of gptFiles) { 53 | const script = await gpt().parse(`${SCRIPTS_PATH()}/${file}`); 54 | let description = ''; 55 | for (const tool of script) { 56 | if (tool.type === 'text') continue; 57 | description = tool.description || ''; 58 | break; 59 | } 60 | scripts[file] = description || ''; 61 | } 62 | 63 | return scripts; 64 | } catch (e) { 65 | const error = e as NodeJS.ErrnoException; 66 | if (error.code === 'ENOENT') return {} as Record; 67 | throw e; 68 | } 69 | }; 70 | 71 | export const fetchScriptCode = async (file: string): Promise => { 72 | file = file.includes('.gpt') ? file : `${file}.gpt`; 73 | try { 74 | return await fs.readFile(`${SCRIPTS_PATH()}/${file}`, 'utf-8'); 75 | } catch (e) { 76 | throw e; 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /components/navbar/me/login.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useContext, useEffect, useState } from 'react'; 4 | import { 5 | AuthProvider, 6 | getAuthProviders, 7 | loginThroughTool, 8 | } from '@/actions/auth/auth'; 9 | import { Button } from '@nextui-org/react'; 10 | import { AuthContext } from '@/contexts/auth'; 11 | import { getMe } from '@/actions/me/me'; 12 | 13 | export default function Login() { 14 | const [authProviders, setAuthProviders] = useState< 15 | AuthProvider[] | undefined 16 | >(undefined); 17 | const [errorMessage, setErrorMessage] = useState(''); 18 | const { setMe } = useContext(AuthContext); 19 | 20 | useEffect(() => { 21 | getAuthProviders() 22 | .then((aps) => { 23 | setAuthProviders(aps); 24 | }) 25 | .catch((reason) => { 26 | console.log(reason); 27 | setErrorMessage(`failed to get auth providers: ${reason}`); 28 | }); 29 | }, []); 30 | 31 | function handleLogin(e: React.MouseEvent) { 32 | e.preventDefault(); 33 | loginThroughTool() 34 | .then(() => getMe().then((me) => setMe(me))) 35 | .catch((reason) => setErrorMessage(`failed to login: ${reason}`)); 36 | } 37 | 38 | if (authProviders === undefined) { 39 | return

Loading...

; 40 | } 41 | if (authProviders.length === 0) { 42 | return ( 43 |
44 |

No Auth providers found

45 |

46 | You can create an auth provider. 47 |

48 |
49 | ); 50 | } 51 | return ( 52 |
53 |
54 |

Welcome!

55 |

To get started, please select an auth provider to login with.

56 | {errorMessage &&

{errorMessage}

} 57 |
58 |
59 | {authProviders.map((authProvider) => { 60 | if (!authProvider.disabled) { 61 | return ( 62 | 73 | ); 74 | } else { 75 | return null; 76 | } 77 | })} 78 |
79 |
80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /components/chat/chatBar/upload/files.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useContext, useCallback } from 'react'; 2 | import { GoTrash, GoFile } from 'react-icons/go'; 3 | import { 4 | Table, 5 | TableBody, 6 | TableHeader, 7 | TableRow, 8 | TableCell, 9 | TableColumn, 10 | Button, 11 | } from '@nextui-org/react'; 12 | import { deleteFile, lsFiles } from '@/actions/upload'; 13 | import { Dirent } from 'fs'; 14 | import { ChatContext } from '@/contexts/chat'; 15 | import path from 'path'; 16 | 17 | interface FilesProps { 18 | files: Dirent[]; 19 | setFiles: React.Dispatch>; 20 | } 21 | 22 | const Files: React.FC = ({ files, setFiles }) => { 23 | useEffect(() => fetchFiles(), []); 24 | const { workspace } = useContext(ChatContext); 25 | 26 | const fetchFiles = useCallback(() => { 27 | lsFiles(workspace) 28 | .then((data: string) => setFiles(JSON.parse(data) as Dirent[])) 29 | .catch((error) => console.error('Error fetching files:', error)); 30 | }, [workspace]); 31 | 32 | return ( 33 | 34 | 35 | File 36 | Actions 37 | 38 | 39 | {files.map((file) => ( 40 | 41 | {file.name} 42 | 43 |
44 |
68 |
69 |
70 | ))} 71 |
72 |
73 | ); 74 | }; 75 | 76 | export default Files; 77 | -------------------------------------------------------------------------------- /components/chat/messages/confirmForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GoCheckCircle, GoXCircle, GoCheckCircleFill } from 'react-icons/go'; 3 | import type { AuthResponse } from '@gptscript-ai/gptscript'; 4 | import { Button, Code, Tooltip } from '@nextui-org/react'; 5 | import Markdown from 'react-markdown'; 6 | import remarkGfm from 'remark-gfm'; 7 | 8 | type ConfirmFormProps = { 9 | id: string; 10 | tool: string; 11 | message?: string; 12 | command?: string; 13 | onSubmit: (data: AuthResponse) => void; 14 | addTrusted: () => void; 15 | }; 16 | 17 | const ConfirmForm = ({ 18 | id, 19 | onSubmit, 20 | addTrusted, 21 | message, 22 | command, 23 | }: ConfirmFormProps) => { 24 | const onSubmitForm = (accept: boolean) => { 25 | onSubmit({ id, message: `denied by user`, accept }); 26 | }; 27 | 28 | return ( 29 |
30 | 34 | {message} 35 | 36 | {command && ( 37 | 38 | {command.startsWith('Running') 39 | ? command.replace('Running', '').replace(/`/g, '') 40 | : command.replace(/`/g, '')} 41 | 42 | )} 43 |
44 | 49 | 58 | 59 | 64 | 75 | 76 | 84 |
85 |
86 | ); 87 | }; 88 | 89 | export default ConfirmForm; 90 | -------------------------------------------------------------------------------- /public/squirrel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/build/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React, { useContext, useEffect, useState } from 'react'; 4 | import { AuthContext } from '@/contexts/auth'; 5 | import Scripts from '@/components/scripts'; 6 | import Loading from '@/components/loading'; 7 | import { NavContext } from '@/contexts/nav'; 8 | import { GoPeople } from 'react-icons/go'; 9 | import Create from '@/components/scripts/create'; 10 | import { Button, Divider, useDisclosure } from '@nextui-org/react'; 11 | import { MdOutlineTravelExplore } from 'react-icons/md'; 12 | import ExploreModal from '@/components/explore/ExploreModal'; 13 | 14 | export default function Home() { 15 | const { loading } = useContext(AuthContext); 16 | const { setCurrent } = useContext(NavContext); 17 | const { isOpen, onOpen, onClose } = useDisclosure(); 18 | const [showFavorites, setShowFavorites] = useState(false); 19 | 20 | useEffect(() => setCurrent('/build'), []); 21 | if (loading) return ; 22 | return ( 23 |
24 |
25 |
26 |

27 | My Assistants 28 |

29 | 30 |
31 | 32 | 45 |
46 |
47 | 48 |
49 |
setShowFavorites(false)} 52 | > 53 | My Assistants 54 |
55 | 56 | 57 | 58 |
setShowFavorites(true)} 61 | > 62 | Favorites 63 |
64 |
65 |
66 | 67 |
68 | 69 |
70 | 71 | 72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /components/chat/messages/calls.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import CallFrames from './calls/callFrames'; 3 | import type { CallFrame } from '@gptscript-ai/gptscript'; 4 | import { IoCloseSharp } from 'react-icons/io5'; 5 | import { BsArrowsFullscreen } from 'react-icons/bs'; 6 | import { HiOutlineArrowsPointingIn } from 'react-icons/hi2'; 7 | import { GoProjectRoadmap } from 'react-icons/go'; 8 | import { 9 | Modal, 10 | ModalContent, 11 | ModalHeader, 12 | ModalBody, 13 | Button, 14 | Tooltip, 15 | } from '@nextui-org/react'; 16 | import SaveFile from '@/components/saveFile'; 17 | 18 | const Calls = ({ calls }: { calls: Record }) => { 19 | const [showModal, setShowModal] = useState(false); 20 | const [fullscreen, setFullscreen] = useState(false); 21 | 22 | return ( 23 |
24 | 25 | 34 | 35 | 42 | 43 | 44 |
45 |
46 |

Call Frames

47 | 48 |
49 |

50 | Below you can see what this call is doing or has done. 51 |

52 |
53 |
54 | 63 | 77 |
78 |
79 | 80 | 81 | 82 |
83 |
84 |
85 | ); 86 | }; 87 | 88 | export default Calls; 89 | -------------------------------------------------------------------------------- /components/edit/configure/params.tsx: -------------------------------------------------------------------------------- 1 | import { Property } from '@gptscript-ai/gptscript'; 2 | import { Input, Button } from '@nextui-org/react'; 3 | import { GoPlus, GoTrash } from 'react-icons/go'; 4 | 5 | interface ExternalProps { 6 | params: Record | undefined; 7 | setParams: (params: Record) => void; 8 | className?: string; 9 | } 10 | 11 | const Imports: React.FC = ({ params, setParams, className }) => { 12 | const handleDeleteParam = (param: string) => { 13 | const updatedParams = { ...params }; 14 | delete updatedParams[param]; 15 | setParams(updatedParams); 16 | }; 17 | 18 | const handleCreateDefaultParam = () => { 19 | let defaultParamName = 'New Param'; 20 | let counter = 1; 21 | while (params && Object.keys(params)?.includes(defaultParamName)) { 22 | defaultParamName = `New Param ${counter}`; 23 | counter++; 24 | } 25 | 26 | setParams({ 27 | ...(params || {}), 28 | [defaultParamName]: { type: 'string', description: '' }, 29 | }); 30 | }; 31 | 32 | return ( 33 |
34 | {params && 35 | Object.keys(params).map((param) => ( 36 |
37 | { 45 | const target = e.target as HTMLInputElement; 46 | const updatedParams = { ...params }; 47 | delete updatedParams[param]; 48 | updatedParams[target.value] = params[param]; 49 | setParams(updatedParams); 50 | }} 51 | /> 52 | { 59 | setParams({ 60 | ...params, 61 | [param]: { 62 | ...params[param], 63 | description: e.target.value, 64 | }, 65 | }); 66 | }} 67 | /> 68 |
76 | ))} 77 |
78 |
88 |
89 | ); 90 | }; 91 | 92 | export default Imports; 93 | -------------------------------------------------------------------------------- /components/chat/chatBar/upload/workspace.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useContext, useEffect } from 'react'; 2 | import { ChatContext } from '@/contexts/chat'; 3 | import { 4 | Modal, 5 | ModalContent, 6 | ModalHeader, 7 | ModalBody, 8 | ModalFooter, 9 | Button, 10 | Input, 11 | } from '@nextui-org/react'; 12 | import { updateThreadWorkspace } from '@/actions/threads'; 13 | 14 | interface WorkspaceProps { 15 | onRestart: () => void; 16 | } 17 | 18 | const Workspace = ({ onRestart }: WorkspaceProps) => { 19 | const [isOpen, setIsOpen] = useState(false); 20 | const { workspace, setWorkspace, thread } = useContext(ChatContext); 21 | const [workspaceInput, setWorkspaceInput] = useState(''); 22 | 23 | useEffect(() => { 24 | setWorkspaceInput(workspace); 25 | }, [workspace]); 26 | 27 | const handleOpen = () => setIsOpen(true); 28 | const handleClose = () => setIsOpen(false); 29 | const handleConfirm = useCallback(() => { 30 | setWorkspace(workspaceInput); 31 | setIsOpen(false); 32 | // Here we use selectedThreadId here because it is always set after thread is created. Thread might not be set when it is created on the fly. 33 | if (thread) { 34 | updateThreadWorkspace(thread, workspace); 35 | } 36 | onRestart(); 37 | }, [workspaceInput]); 38 | 39 | return ( 40 |
41 | 49 | event.key === 'Enter' && 50 | workspaceInput != workspace && 51 | handleConfirm() 52 | } 53 | onChange={(event) => setWorkspaceInput(event.target.value)} 54 | /> 55 | {workspace != workspaceInput && ( 56 | 63 | )} 64 | 71 | 72 | 73 |

Are you sure?

74 |
75 | 76 |

77 | Changing the workspace for this script requires it to restart. 78 |

79 |
80 | 81 | 84 | 87 | 88 |
89 |
90 |
91 | ); 92 | }; 93 | 94 | export default Workspace; 95 | -------------------------------------------------------------------------------- /actions/gptscript.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { ToolDef, Tool, Block, Text, Program } from '@gptscript-ai/gptscript'; 4 | import { gpt } from '@/config/env'; 5 | 6 | const SystemToolWhitelist = [ 7 | 'sys.abort', 8 | 'sys.append', 9 | 'sys.chat.current', 10 | 'sys.chat.finish', 11 | 'sys.chat.history', 12 | 'sys.context', 13 | 'sys.download', 14 | 'sys.exec', 15 | 'sys.find', 16 | 'sys.getenv', 17 | 'sys.http.get', 18 | 'sys.http.html2text', 19 | 'sys.http.post', 20 | 'sys.ls', 21 | 'sys.model.provider.credential', 22 | 'sys.prompt', 23 | 'sys.read', 24 | 'sys.remove', 25 | 'sys.stat', 26 | 'sys.time.now', 27 | 'sys.write', 28 | ]; 29 | 30 | export const rootTool = async (toolContent: Block[]): Promise => { 31 | for (const block of toolContent) { 32 | if (block.type !== 'text') return block; 33 | } 34 | return {} as Tool; 35 | }; 36 | 37 | export const parseContent = async (toolContent: string): Promise => { 38 | const parsedTool = await gpt().parseContent(toolContent); 39 | return parsedTool.filter( 40 | (block) => block.type !== 'text' && !block.name?.startsWith('metadata') 41 | ) as Tool[]; 42 | }; 43 | 44 | /** 45 | * Verifies that a tool exists by parsing it. 46 | * @param toolRef The tool reference to verify. 47 | * @returns A boolean indicating whether the tool exists. 48 | */ 49 | export const verifyToolExists = async (toolRef: string) => { 50 | // skip verification if the tool is a system tool 51 | if (toolRef.startsWith('sys.')) return SystemToolWhitelist.includes(toolRef); 52 | 53 | try { 54 | await gpt().parse(toolRef); 55 | return true; 56 | } catch (_) { 57 | return false; 58 | } 59 | }; 60 | 61 | export const parse = async (file: string): Promise => { 62 | const parsedTool = await gpt().parse(file); 63 | return parsedTool.filter( 64 | (block) => block.type !== 'text' && !block.name?.startsWith('metadata') 65 | ) as Tool[]; 66 | }; 67 | 68 | export const load = async (file: string): Promise => { 69 | return (await gpt().load(file)).program; 70 | }; 71 | 72 | export const getToolDisplayName = async (ref: string) => { 73 | if (ref.startsWith('sys.')) return null; 74 | 75 | try { 76 | const toolDef = await load(ref); 77 | return toolDef.toolSet[toolDef.entryToolId].name; 78 | } catch (e) { 79 | console.error('Error loading tool:', e); 80 | return null; 81 | } 82 | }; 83 | 84 | export const loadTools = async ( 85 | tools: ToolDef | ToolDef[] 86 | ): Promise => { 87 | try { 88 | if (Array.isArray(tools)) return (await gpt().loadTools(tools)).program; 89 | return (await gpt().loadTools([tools])).program; 90 | } catch (e) { 91 | console.log(`Error loading tools: ${e}`); 92 | } 93 | 94 | return {} as Program; 95 | }; 96 | 97 | export const getTexts = async (toolContent: string): Promise => { 98 | const parsedTool = await gpt().parseContent(toolContent); 99 | return parsedTool.filter((block) => block.type === 'text') as Text[]; 100 | }; 101 | 102 | export const stringify = async (script: Block[]): Promise => { 103 | return await gpt().stringify(script); 104 | }; 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "acorn", 3 | "displayName": "Acorn", 4 | "author": { 5 | "name": "Acorn Labs, Inc.", 6 | "email": "info@acorn.io", 7 | "url": "https://acorn.io" 8 | }, 9 | "private": true, 10 | "version": "v0.10.0-rc4", 11 | "scripts": { 12 | "build": "next build --no-lint", 13 | "build:electron": "next build --no-lint && node electron/build.mjs", 14 | "pkg": "node electron/build.mjs", 15 | "debug": "node --inspect server.mjs", 16 | "dev": "node server.mjs", 17 | "dev:electron": "electron .", 18 | "lint": "next lint", 19 | "format": "prettier --write .", 20 | "start": "cross-env NODE_ENV='production' node server.mjs", 21 | "postinstall": "node scripts/install-binary.mjs" 22 | }, 23 | "main": "electron/main.mjs", 24 | "dependencies": { 25 | "@gptscript-ai/gptscript": "github:gptscript-ai/node-gptscript#1cf71a66d0ca7cffe98343bf672f54a6a84d9ed0", 26 | "@monaco-editor/react": "^4.6.0", 27 | "@nextui-org/button": "2.0.32", 28 | "@nextui-org/code": "2.0.28", 29 | "@nextui-org/dropdown": "2.1.24", 30 | "@nextui-org/input": "2.2.0", 31 | "@nextui-org/kbd": "2.0.29", 32 | "@nextui-org/link": "2.0.30", 33 | "@nextui-org/navbar": "2.0.31", 34 | "@nextui-org/react": "2.4.6", 35 | "@nextui-org/snippet": "2.0.36", 36 | "@nextui-org/switch": "2.0.29", 37 | "@nextui-org/system": "2.1.0", 38 | "@nextui-org/theme": "2.2.9", 39 | "@react-aria/ssr": "^3.9.2", 40 | "@react-aria/visually-hidden": "^3.8.10", 41 | "@tailwindcss/typography": "^0.5.13", 42 | "@types/archiver": "^6.0.2", 43 | "@types/node": "20.5.7", 44 | "@types/react": "18.2.21", 45 | "@types/react-dom": "18.2.7", 46 | "archiver": "^7.0.1", 47 | "autoprefixer": "10.4.19", 48 | "clsx": "^2.0.0", 49 | "dotenv": "^16.4.5", 50 | "doubly-linked-list": "^1.0.6", 51 | "electron-log": "^5.1.7", 52 | "eslint-config-next": "14.2.1", 53 | "fix-path": "^4.0.0", 54 | "framer-motion": "^11.1.1", 55 | "fuse.js": "^7.0.0", 56 | "get-port-please": "^3.1.2", 57 | "intl-messageformat": "^10.5.0", 58 | "lodash": "^4.17.21", 59 | "next": "14.2.1", 60 | "next-themes": "^0.2.1", 61 | "open": "^10.1.0", 62 | "postcss": "8.4.38", 63 | "react": "18.2.0", 64 | "react-dom": "18.2.0", 65 | "react-hook-form": "^7.51.5", 66 | "react-icons": "^5.1.0", 67 | "react-json-tree": "^0.19.0", 68 | "react-markdown": "^9.0.1", 69 | "react-svg": "^16.1.34", 70 | "rehype-external-links": "^3.0.0", 71 | "remark-gfm": "^4.0.0", 72 | "socket.io": "^4.7.5", 73 | "socket.io-client": "^4.7.5", 74 | "tailwind-variants": "^0.1.20", 75 | "tailwindcss": "3.4.3", 76 | "typescript": "^5.5.4", 77 | "typescript-collections": "^1.3.3", 78 | "use-file-picker": "^2.1.2" 79 | }, 80 | "devDependencies": { 81 | "@typescript-eslint/eslint-plugin": "^8.1.0", 82 | "@typescript-eslint/parser": "^8.1.0", 83 | "concurrently": "^8.2.2", 84 | "cross-env": "^7.0.3", 85 | "electron": "^31.3.1", 86 | "electron-builder": "^24.13.3", 87 | "eslint": "^8.57.0", 88 | "eslint-config-prettier": "^9.1.0", 89 | "eslint-plugin-import": "^2.29.1", 90 | "eslint-plugin-prettier": "^5.2.1" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /public/lightmode_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /public/darkmode_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /actions/auth/auth.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { cookies } from 'next/headers'; 4 | import { create, get, list } from '@/actions/common'; 5 | import { gpt } from '@/config/env'; 6 | import { RunEventType, ToolDef } from '@gptscript-ai/gptscript'; 7 | 8 | const tokenRequestToolInstructions = ` 9 | Credential: github.com/gptscript-ai/gateway-creds as github.com/gptscript-ai/gateway 10 | Name: getCreds 11 | 12 | #!/usr/bin/env python3 13 | 14 | import os 15 | import json 16 | 17 | output = { 18 | "token": os.getenv("GPTSCRIPT_GATEWAY_API_KEY", ""), 19 | "expiresAt": os.getenv("GPTSCRIPT_CREDENTIAL_EXPIRATION", ""), 20 | } 21 | 22 | print(json.dumps(output), end="") 23 | 24 | --- 25 | 26 | !metadata:getCreds:requirements.txt 27 | 28 | `; 29 | 30 | export interface AuthProvider { 31 | id?: string; 32 | type: string; 33 | serviceName?: string; 34 | slug?: string; 35 | clientID?: string; 36 | clientSecret?: string; 37 | oauthURL?: string; 38 | tokenURL?: string; 39 | scopes?: string; 40 | redirectURL?: string; 41 | disabled?: boolean; 42 | } 43 | 44 | export async function setCookies( 45 | token: string, 46 | expiresAt: string 47 | ): Promise { 48 | cookies().set('gateway_token', token, { domain: 'localhost' }); 49 | cookies().set('expires_at', expiresAt, { domain: 'localhost' }); 50 | } 51 | 52 | export async function logout(): Promise { 53 | cookies().delete('gateway_token'); 54 | } 55 | 56 | export async function getAuthProviders(): Promise { 57 | return await list('auth-providers'); 58 | } 59 | 60 | export async function createTokenRequest( 61 | id: string, 62 | oauthServiceName: string 63 | ): Promise { 64 | return ( 65 | await create( 66 | { id: id, serviceName: oauthServiceName } as any, 67 | 'token-request' 68 | ) 69 | )['token-path']; 70 | } 71 | 72 | /* 73 | This function is used to login through a GPTScript tool. This is useful because GPTScript will handle the 74 | storing of this token on our behalf as well as the OAuth flow. 75 | 76 | This loginTimeout is used to track the current logim timeout. If the token is about to expire, we will 77 | automatically log in again. We have an in-memory timeout to prevent multiple expiration checks from 78 | happening at the same time. 79 | */ 80 | let loginTimeout: NodeJS.Timeout | null = null; 81 | export async function loginThroughTool(): Promise { 82 | if (loginTimeout) { 83 | clearTimeout(loginTimeout); 84 | loginTimeout = null; 85 | } 86 | 87 | const run = await gpt().evaluate( 88 | { instructions: tokenRequestToolInstructions } as ToolDef, 89 | { prompt: true } 90 | ); 91 | run.on(RunEventType.Prompt, (data) => { 92 | gpt().promptResponse({ id: data.id, responses: {} }); 93 | }); 94 | 95 | const response = JSON.parse(await run.text()) as { 96 | token: string; 97 | expiresAt: string; 98 | }; 99 | setCookies(response.token, response.expiresAt); 100 | 101 | loginTimeout = setTimeout( 102 | () => { 103 | loginTimeout = null; 104 | loginThroughTool(); 105 | }, 106 | new Date(response.expiresAt).getTime() - Date.now() - 1000 * 60 * 5 107 | ); 108 | } 109 | 110 | export async function pollForToken(id: string): Promise { 111 | // eslint-disable-next-line no-constant-condition 112 | while (true) { 113 | const token = (await get('token-request', id)).token || ''; 114 | if (token != '') { 115 | return token; 116 | } 117 | 118 | await new Promise((r) => setTimeout(r, 1000)); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /electron/config.mjs: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import { getPort } from 'get-port-please'; 3 | import { join, dirname, parse } from 'path'; 4 | import log from 'electron-log/main.js'; 5 | import util from 'util'; 6 | import { renameSync } from 'fs'; 7 | import os from 'os'; 8 | 9 | function appSettingsLocation() { 10 | const homeDir = os.homedir(); 11 | let configDir; 12 | if (os.platform() === 'darwin') { 13 | configDir = 14 | process.env.XDG_CONFIG_HOME || 15 | join(homeDir, 'Library', 'Application Support'); 16 | } else if (os.platform() === 'win32') { 17 | configDir = join(homeDir, 'AppData', 'Local'); 18 | } else if (os.platform() === 'linux') { 19 | configDir = process.env.XDG_CONFIG_HOME || join(homeDir, '.config'); 20 | } else { 21 | throw new Error('Unsupported platform'); 22 | } 23 | 24 | return join(configDir, 'acorn', 'settings.json'); 25 | } 26 | 27 | // App config 28 | const dev = !app.isPackaged; 29 | const appName = 'Acorn'; 30 | const appDir = app.getAppPath(); 31 | const logsDir = app.getPath('logs'); 32 | const resourcesDir = dirname(appDir); 33 | const dataDir = join(app.getPath('userData'), appName); 34 | const threadsDir = process.env.THREADS_DIR || join(dataDir, 'threads'); 35 | const workspaceDir = process.env.WORKSPACE_DIR || join(dataDir, 'workspace'); 36 | const appSettingsFile = appSettingsLocation(); 37 | const port = 38 | process.env.PORT || 39 | (!dev ? await getPort({ portRange: [30000, 40000] }) : 3000); 40 | const gptscriptBin = 41 | process.env.GPTSCRIPT_BIN || 42 | join( 43 | !dev ? join(resourcesDir, 'app.asar.unpacked') : '', 44 | 'node_modules', 45 | '@gptscript-ai', 46 | 'gptscript', 47 | 'bin', 48 | `gptscript${process.platform === 'win32' ? '.exe' : ''}` 49 | ); 50 | const knowledgeBin = 51 | process.env.KNOWLEDGE_BIN || 52 | (process.env.NODE_ENV === 'production' 53 | ? join( 54 | process.resourcesPath, 55 | 'bin', 56 | 'knowledge' + (process.platform === 'win32' ? '.exe' : '') 57 | ) 58 | : join(process.cwd(), 'bin', 'knowledge')); 59 | const gatewayUrl = 60 | process.env.GPTSCRIPT_GATEWAY_URL || 'https://gateway-api.gptscript.ai'; 61 | 62 | // Logging config 63 | const logFormat = ({ data, level, message }) => [ 64 | message.date.toISOString(), 65 | `[${message.variables.processType === 'main' ? 'server' : 'client'}]`, 66 | `[${level.toUpperCase()}]`, 67 | util.format(...data), 68 | ]; 69 | 70 | log.transports.console.format = logFormat; 71 | 72 | Object.assign(log.transports.file, { 73 | format: logFormat, 74 | resolvePathFn: (variables) => { 75 | return join(logsDir, `${variables.appName}.log`); 76 | }, 77 | archiveLogFn: (file) => { 78 | const filePath = file.toString(); 79 | const info = parse(filePath); 80 | const timestamp = Math.floor(Date.now() / 1000); 81 | 82 | try { 83 | renameSync( 84 | filePath, 85 | join(info.dir, `${info.name}.${timestamp}${info.ext}`) 86 | ); 87 | } catch (e) { 88 | console.warn('failed to rotate log file', e); 89 | } 90 | }, 91 | }); 92 | 93 | log.initialize({ 94 | // Include logs gathered from clients via IPC 95 | spyRendererConsole: true, 96 | includeFutureSessions: true, 97 | }); 98 | 99 | // Forward default console logging to electron-log 100 | Object.assign(console, log.functions); 101 | 102 | export const config = { 103 | dev, 104 | appName, 105 | logsDir, 106 | appDir, 107 | resourcesDir, 108 | dataDir, 109 | threadsDir, 110 | workspaceDir, 111 | port, 112 | gptscriptBin, 113 | gatewayUrl, 114 | knowledgeBin, 115 | appSettingsFile, 116 | }; 117 | -------------------------------------------------------------------------------- /electron/build.mjs: -------------------------------------------------------------------------------- 1 | import builder from 'electron-builder'; 2 | import os from 'os'; 3 | 4 | const Platform = builder.Platform; 5 | 6 | /** 7 | * @type {import('electron-builder').Configuration} 8 | */ 9 | const options = { 10 | appId: 'ai.gptscript.acorn', 11 | productName: 'Acorn', 12 | files: [ 13 | '**/*', 14 | '!**/node_modules/*/{CHANGELOG.md,README.md,README,readme.md,readme}', 15 | '!**/node_modules/*/{test,__tests__,tests,powered-test,example,examples}', 16 | '!**/node_modules/.bin', 17 | '!**/*.{iml,o,hprof,orig,pyc,pyo,rbc,swp,csproj,sln,xproj}', 18 | '!.editorconfig', 19 | '!**/._*', 20 | '!**/.next/cache', 21 | '!**/node_modules/@next/swc*', 22 | '!**/node_modules/@next/swc*/**', 23 | '!**/{.DS_Store,.git,.github,*.zip,*.tar.gz,.hg,.svn,CVS,RCS,SCCS,.gitignore,.vscode,.gitattributes,package-lock.json}', 24 | '!**/{__pycache__,thumbs.db,.flowconfig,.idea,.vs,.nyc_output}', 25 | '!**/{appveyor.yml,.travis.yml,circle.yml}', 26 | '!**/{npm-debug.log,yarn.lock,.yarn-integrity,.yarn-metadata.json}', 27 | 'bin/*', 28 | ], 29 | mac: { 30 | hardenedRuntime: true, 31 | gatekeeperAssess: false, 32 | entitlements: 'electron/entitlements.mac.plist', 33 | entitlementsInherit: 'electron/entitlements.mac.plist', 34 | icon: 'electron/icon.icns', 35 | category: 'public.app-category.productivity', 36 | target: 'dmg', 37 | }, 38 | dmg: { 39 | writeUpdateInfo: false, 40 | }, 41 | win: { 42 | artifactName: '${productName}-Setup-${version}.${ext}', 43 | target: { 44 | target: 'nsis', 45 | }, 46 | icon: 'electron/icon.ico', 47 | }, 48 | nsis: { 49 | deleteAppDataOnUninstall: true, 50 | differentialPackage: false, 51 | }, 52 | linux: { 53 | maintainer: 'Acorn Labs', 54 | category: 'Office', 55 | desktop: { 56 | StartupNotify: 'false', 57 | Encoding: 'UTF-8', 58 | MimeType: 'x-scheme-handler/deeplink', 59 | }, 60 | icon: 'electron/icon.png', 61 | target: ['AppImage'], 62 | }, 63 | compression: 'normal', 64 | removePackageScripts: true, 65 | nodeGypRebuild: false, 66 | buildDependenciesFromSource: false, 67 | directories: { 68 | output: 'electron-dist', 69 | }, 70 | publish: { 71 | provider: 'github', 72 | publishAutoUpdate: false, 73 | releaseType: 'release', 74 | vPrefixedTagName: true, 75 | }, 76 | extraResources: [ 77 | { 78 | from: 'bin/', 79 | to: 'bin', 80 | filter: ['**/*'], 81 | }, 82 | ], 83 | }; 84 | 85 | function go() { 86 | const platform = os.platform(); 87 | const arch = os.arch(); 88 | 89 | let targetPlatform; 90 | switch (platform) { 91 | case 'darwin': 92 | targetPlatform = 'mac'; 93 | break; 94 | case 'win32': 95 | targetPlatform = 'windows'; 96 | break; 97 | case 'linux': 98 | targetPlatform = 'linux'; 99 | break; 100 | default: 101 | throw new Error(`Unsupported platform: ${platform}`); 102 | } 103 | console.log(`targetPlatform: ${targetPlatform}`); 104 | 105 | // Only publish when the GH_TOKEN is set. 106 | // This indicates the intent to publish the build to a release. 107 | const publishOption = process.env.GH_TOKEN ? 'always' : 'never'; 108 | 109 | builder 110 | .build({ 111 | targets: Platform[targetPlatform.toUpperCase()].createTarget(), 112 | config: options, 113 | publish: publishOption, 114 | }) 115 | .then((result) => { 116 | console.info('----------------------------'); 117 | console.info('Platform:', platform); 118 | console.info('Architecture:', arch); 119 | console.info('Output:', JSON.stringify(result, null, 2)); 120 | }); 121 | } 122 | 123 | go(); 124 | -------------------------------------------------------------------------------- /components/navbar/me.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Popover, 5 | PopoverTrigger, 6 | PopoverContent, 7 | Button, 8 | User, 9 | Divider, 10 | } from '@nextui-org/react'; 11 | import { useState, useEffect, useContext } from 'react'; 12 | import { GoPersonFill, GoGear } from 'react-icons/go'; 13 | import Logout from '@/components/navbar/me/logout'; 14 | import { AuthContext } from '@/contexts/auth'; 15 | import { loginThroughTool } from '@/actions/auth/auth'; 16 | import { getMe } from '@/actions/me/me'; 17 | import Loading from '@/components/loading'; 18 | import NextLink from 'next/link'; 19 | 20 | interface MeProps { 21 | className?: string; 22 | } 23 | 24 | const Me = ({ className }: MeProps) => { 25 | const [isOpen, setIsOpen] = useState(false); 26 | const { authenticated, me, setMe, setAuthenticated } = 27 | useContext(AuthContext); 28 | const [_open, setOpen] = useState(false); 29 | const [loading, setLoading] = useState(false); 30 | 31 | useEffect(() => { 32 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 33 | authenticated && setOpen(false); 34 | }, [authenticated]); 35 | 36 | function handleLogin() { 37 | setLoading(true); 38 | loginThroughTool() 39 | .then(() => 40 | getMe().then((me) => { 41 | setMe(me); 42 | setAuthenticated(true); 43 | }) 44 | ) 45 | .finally(() => setLoading(false)); 46 | } 47 | 48 | return ( 49 | <> 50 | setIsOpen(open)} 55 | > 56 | 57 | 100 | )} 101 | {authenticated && ( 102 | <> 103 | 113 | 114 | )} 115 | 116 | )} 117 | 118 | 119 | 120 | ); 121 | }; 122 | 123 | export default Me; 124 | -------------------------------------------------------------------------------- /components/edit/scriptNav.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useContext } from 'react'; 2 | import { 3 | Dropdown, 4 | DropdownTrigger, 5 | DropdownMenu, 6 | DropdownItem, 7 | DropdownSection, 8 | Button, 9 | } from '@nextui-org/react'; 10 | import { IoMenu } from 'react-icons/io5'; 11 | import { FaCopy } from 'react-icons/fa'; 12 | import { VscNewFile } from 'react-icons/vsc'; 13 | import { GoPerson, GoSidebarCollapse, GoSidebarExpand } from 'react-icons/go'; 14 | import { ParsedScript, getScripts } from '@/actions/me/scripts'; 15 | import { EditContext } from '@/contexts/edit'; 16 | import { AuthContext } from '@/contexts/auth'; 17 | import { stringify } from '@/actions/gptscript'; 18 | import { createDefaultAssistant } from '@/actions/me/scripts'; 19 | 20 | interface ScriptNavProps { 21 | className?: string; 22 | collapsed: boolean; 23 | setCollapsed?: React.Dispatch>; 24 | } 25 | 26 | const ScriptNav: React.FC = ({ collapsed, setCollapsed }) => { 27 | const [scripts, setScripts] = useState([]); 28 | const { script, loading } = useContext(EditContext); 29 | const { me } = useContext(AuthContext); 30 | 31 | const handleNew = async () => { 32 | createDefaultAssistant().then((script) => { 33 | window.location.href = `/edit?id=${script?.id}`; 34 | }); 35 | }; 36 | 37 | useEffect(() => { 38 | if (!me) return; 39 | getScripts({ owner: me?.username }) 40 | .then((resp) => setScripts(resp.scripts || [])) 41 | .catch((error) => console.error(error)); 42 | }, [me]); 43 | 44 | const ScriptItems = 45 | scripts && scripts.length ? ( 46 | scripts.map((script, i) => ( 47 | } 49 | key={script.id} 50 | > 51 | {script.agentName || `Untitled Assistant ${i}`} 52 | 53 | )) 54 | ) : ( 55 | 56 | No files found 57 | 58 | ); 59 | 60 | return ( 61 | 62 | 63 | 73 | 74 | { 77 | if (key === 'collapse') { 78 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 79 | setCollapsed && setCollapsed(!collapsed); 80 | } else if (key === 'export') { 81 | stringify(script).then((gptscript) => { 82 | navigator.clipboard.writeText(gptscript); 83 | }); 84 | } else if (key === 'new') { 85 | handleNew(); 86 | } else { 87 | window.location.href = `/edit?id=${key}`; 88 | } 89 | }} 90 | disabledKeys={['no-files']} 91 | > 92 | : } 95 | key="collapse" 96 | > 97 | {collapsed ? 'Expand' : 'Collapse'} editor 98 | 99 | 100 | } key="new"> 101 | New assistant 102 | 103 | } key="export"> 104 | Copy assistant to clipboard 105 | 106 | 107 | {ScriptItems} 108 | 109 | 110 | ); 111 | }; 112 | 113 | export default ScriptNav; 114 | -------------------------------------------------------------------------------- /actions/me/scripts.tsx: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { create, del, get, list, update } from '@/actions/common'; 4 | import { GATEWAY_URL, gpt } from '@/config/env'; 5 | import { Block } from '@gptscript-ai/gptscript'; 6 | 7 | export interface ParsedScript extends Script { 8 | agentName?: string; 9 | description?: string; 10 | script: Block[]; 11 | } 12 | 13 | export interface Script { 14 | displayName?: string; 15 | createdAt?: string; 16 | updatedAt?: string; 17 | content?: string; 18 | id?: number; 19 | owner?: string; 20 | tags?: string[]; 21 | visibility?: string; 22 | publicURL?: string; 23 | slug?: string; 24 | } 25 | 26 | export interface ScriptsQuery { 27 | owner?: string; 28 | filter?: string; 29 | limit?: number; 30 | continue?: string; 31 | search?: string; 32 | visibility?: 'public' | 'private' | undefined; 33 | } 34 | 35 | export interface ScriptsQueryResponse { 36 | continue?: string; 37 | scripts?: Script[]; 38 | } 39 | 40 | export interface ParsedScriptsQueryResponse { 41 | continue?: string; 42 | scripts?: ParsedScript[]; 43 | } 44 | 45 | // note: can combine these two functions into one to save cycles 46 | function getDescription(script: Block[]): string { 47 | for (const tool of script) { 48 | if (tool.type === 'text') continue; 49 | return tool.description || ''; 50 | } 51 | return ''; 52 | } 53 | function getName(script: Block[]): string { 54 | for (const tool of script) { 55 | if (tool.type === 'text') continue; 56 | return tool.name || ''; 57 | } 58 | return ''; 59 | } 60 | 61 | export async function getScripts( 62 | query?: ScriptsQuery 63 | ): Promise { 64 | let scripts: ScriptsQueryResponse = {}; 65 | if (!query) scripts = await list('scripts'); 66 | else 67 | scripts = await list( 68 | 'scripts?' + new URLSearchParams(query as any).toString() 69 | ); 70 | 71 | const parsedScripts: ParsedScript[] = []; 72 | for (const script of scripts.scripts || []) { 73 | const parsedScript = await gpt().parseContent(script.content || ''); 74 | 75 | parsedScripts.push({ 76 | ...script, 77 | script: parsedScript, 78 | description: getDescription(parsedScript), 79 | agentName: getName(parsedScript), 80 | }); 81 | } 82 | 83 | return { continue: scripts.continue, scripts: parsedScripts }; 84 | } 85 | 86 | export async function getScript(id: string): Promise { 87 | try { 88 | const scripts = (await get( 89 | 'scripts', 90 | id.replace(`${GATEWAY_URL()}/`, '') 91 | )) as Script; 92 | const parsedScript = await gpt().parseContent(scripts.content || ''); 93 | return { 94 | ...scripts, 95 | script: parsedScript, 96 | description: getDescription(parsedScript), 97 | agentName: getName(parsedScript), 98 | }; 99 | } catch (e) { 100 | return undefined; 101 | } 102 | } 103 | 104 | export async function createScript(script: Script): Promise