├── supabase ├── seed.sql ├── .gitignore ├── migrations │ ├── 20240711215150_disable_roles.sql │ ├── 20240711010550_remote_schema.sql │ ├── 20240711011456_enable_auth.sql │ ├── 20240714042921_change_token_amount.sql │ └── 20240708211409_add_tokens.sql └── config.toml ├── .eslintrc.json ├── bun.lockb ├── app ├── icon.png ├── login │ └── page.tsx ├── api │ ├── trpc │ │ └── [trpc] │ │ │ └── route.ts │ ├── checkout │ │ └── route.ts │ ├── chat │ │ └── route.ts │ ├── stripe │ │ └── webhook │ │ │ └── route.ts │ ├── name │ │ └── route.ts │ ├── help │ │ └── route.ts │ ├── icon │ │ └── route.ts │ └── program │ │ └── route.ts ├── error │ └── page.tsx ├── page.tsx ├── auth │ ├── callback │ │ └── route.ts │ └── confirm │ │ └── route.ts ├── layout.tsx └── globals.css ├── public ├── bg.jpg ├── start.mp3 ├── welcome.jpeg ├── welcome_raw.jpeg ├── vercel.svg ├── reset.css ├── next.svg └── api.js ├── components ├── assets │ ├── x.ico │ ├── disk.png │ ├── up.ico │ ├── check.png │ ├── image.png │ ├── newDir.png │ ├── paste.ico │ └── window.png ├── landing │ ├── assets │ │ ├── banner.png │ │ ├── clock.png │ │ ├── copy.png │ │ ├── done.png │ │ ├── notes.png │ │ ├── restart.png │ │ ├── installer.png │ │ ├── sawyersoft.png │ │ └── sawyersoft.svg │ ├── CountDown.tsx │ ├── Form.tsx │ ├── Landing.module.css │ └── Landing.tsx ├── programs │ ├── updateAssets │ │ ├── mount.png │ │ └── history.png │ ├── Settings.module.css │ ├── Alert.module.css │ ├── History.module.css │ ├── Explorer.module.css │ ├── Alert.tsx │ ├── Welcome.module.css │ ├── History.tsx │ ├── Help.module.css │ ├── Run.tsx │ ├── Settings.tsx │ └── Iframe.tsx ├── ContextMenu.module.css ├── SettingsLink.tsx ├── Window.module.css ├── MenuBar.module.css ├── ContextMenu.tsx ├── WindowBody.tsx ├── Desktop.module.css ├── OS.module.css ├── ModelSection.tsx ├── Desktop.tsx ├── MenuBar.tsx ├── WindowMenuBar.tsx └── OS.tsx ├── lib ├── isLive.ts ├── isLocal.ts ├── assertNever.ts ├── supportsDirectoryPicker.ts ├── log.ts ├── assert.ts ├── isMobile.ts ├── api │ ├── client.ts │ └── APIProvider.tsx ├── extractXMLTag.ts ├── filesystem │ ├── defaultFileSystem.ts │ ├── readFileAsText.ts │ ├── __test__ │ │ └── filterOutKey.ts │ ├── getOldFormat.ts │ ├── RealFs.ts │ ├── Drive.ts │ ├── directoryMapping.ts │ └── FsManager.ts ├── getSettings.ts ├── getRegistryKeys.ts ├── auth │ ├── getUser.ts │ └── actions.ts ├── wrappedFetch.tsx ├── actions │ └── ActionsProvider.tsx ├── getURLForProgram.ts ├── CSPosthogProvider.tsx ├── waitForElement.ts ├── put.ts ├── getSettingsFromRequest.ts ├── supabase │ ├── server.ts │ └── middleware.ts ├── capture.ts ├── initState.ts ├── runProgramFromPath.tsx ├── alert.ts ├── debounce.ts ├── showUpsell.tsx ├── apiText.ts └── createWindow.tsx ├── vercel.json ├── state ├── startMenu.tsx ├── focusedWindow.tsx ├── lastVisitedPath.tsx ├── settings.tsx ├── allWindows.tsx ├── contextMenu.tsx ├── windowsList.tsx ├── registry.ts ├── fsManager.ts └── programs.tsx ├── flags ├── config.ts ├── context.tsx └── flags.ts ├── server ├── trpc.ts ├── paymentRequiredResponse.ts ├── usage │ ├── getTokens.ts │ ├── canGenerate.ts │ ├── insertGeneration.ts │ └── createTransaction.ts └── appRouter.ts ├── scripts ├── lib │ └── scriptClient.ts └── getEmailForID.ts ├── ai ├── getMaxTokens.ts ├── image.ts ├── createCompletion.ts └── client.ts ├── next.config.mjs ├── .gitignore ├── jest.config.ts ├── middleware.ts ├── .github └── workflows │ └── ci.yml ├── tsconfig.json ├── specs └── registry.md ├── package.json ├── iframe └── api.ts └── README.md /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/bun.lockb -------------------------------------------------------------------------------- /app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/app/icon.png -------------------------------------------------------------------------------- /public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/public/bg.jpg -------------------------------------------------------------------------------- /public/start.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/public/start.mp3 -------------------------------------------------------------------------------- /public/welcome.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/public/welcome.jpeg -------------------------------------------------------------------------------- /components/assets/x.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/assets/x.ico -------------------------------------------------------------------------------- /public/welcome_raw.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/public/welcome_raw.jpeg -------------------------------------------------------------------------------- /components/assets/disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/assets/disk.png -------------------------------------------------------------------------------- /components/assets/up.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/assets/up.ico -------------------------------------------------------------------------------- /components/assets/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/assets/check.png -------------------------------------------------------------------------------- /components/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/assets/image.png -------------------------------------------------------------------------------- /components/assets/newDir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/assets/newDir.png -------------------------------------------------------------------------------- /components/assets/paste.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/assets/paste.ico -------------------------------------------------------------------------------- /components/assets/window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/assets/window.png -------------------------------------------------------------------------------- /lib/isLive.ts: -------------------------------------------------------------------------------- 1 | const isLive = process.env.NEXT_PUBLIC_IS_LIVE !== "false"; 2 | 3 | export default isLive; 4 | -------------------------------------------------------------------------------- /lib/isLocal.ts: -------------------------------------------------------------------------------- 1 | export function isLocal() { 2 | return process.env.NEXT_PUBLIC_LOCAL_MODE === "true"; 3 | } 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "app/**/*": { 4 | "maxDuration": 60 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /state/startMenu.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | 3 | export const startMenuOpenAtom = atom(false); 4 | -------------------------------------------------------------------------------- /components/landing/assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/landing/assets/banner.png -------------------------------------------------------------------------------- /components/landing/assets/clock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/landing/assets/clock.png -------------------------------------------------------------------------------- /components/landing/assets/copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/landing/assets/copy.png -------------------------------------------------------------------------------- /components/landing/assets/done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/landing/assets/done.png -------------------------------------------------------------------------------- /components/landing/assets/notes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/landing/assets/notes.png -------------------------------------------------------------------------------- /components/landing/assets/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/landing/assets/restart.png -------------------------------------------------------------------------------- /components/landing/assets/installer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/landing/assets/installer.png -------------------------------------------------------------------------------- /components/landing/assets/sawyersoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/landing/assets/sawyersoft.png -------------------------------------------------------------------------------- /lib/assertNever.ts: -------------------------------------------------------------------------------- 1 | export const assertNever = (value: never): never => { 2 | throw new Error(`Unexpected value: ${value}`); 3 | }; 4 | -------------------------------------------------------------------------------- /components/programs/updateAssets/mount.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/programs/updateAssets/mount.png -------------------------------------------------------------------------------- /flags/config.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | tokens: { 3 | allowed: ["kirbyhood@gmail.com"], 4 | enabled: true, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /lib/supportsDirectoryPicker.ts: -------------------------------------------------------------------------------- 1 | export function supportsDirectoryPicker(): boolean { 2 | return "showDirectoryPicker" in window; 3 | } 4 | -------------------------------------------------------------------------------- /components/programs/updateAssets/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SawyerHood/windows9x/HEAD/components/programs/updateAssets/history.png -------------------------------------------------------------------------------- /lib/log.ts: -------------------------------------------------------------------------------- 1 | export function log(...args: any[]) { 2 | if (process.env.NODE_ENV === "development") { 3 | console.log(...args); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /state/focusedWindow.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { atom } from "jotai"; 3 | 4 | export const focusedWindowAtom = atom(null); 5 | -------------------------------------------------------------------------------- /lib/assert.ts: -------------------------------------------------------------------------------- 1 | export function assert(condition: any, message: string): asserts condition { 2 | if (!condition) { 3 | throw new Error(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/isMobile.ts: -------------------------------------------------------------------------------- 1 | export function isMobile() { 2 | return ( 3 | typeof window !== "undefined" && 4 | (window.innerWidth < 768 || window.innerHeight < 768) 5 | ); 6 | } 7 | -------------------------------------------------------------------------------- /server/trpc.ts: -------------------------------------------------------------------------------- 1 | import { initTRPC } from "@trpc/server"; 2 | 3 | const t = initTRPC.create(); 4 | export const router = t.router; 5 | export const publicProcedure = t.procedure; 6 | -------------------------------------------------------------------------------- /lib/api/client.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from "@trpc/react-query"; 2 | import { type AppRouter } from "@/server/appRouter"; 3 | 4 | export const trpc = createTRPCReact(); 5 | -------------------------------------------------------------------------------- /supabase/migrations/20240711215150_disable_roles.sql: -------------------------------------------------------------------------------- 1 | REVOKE ALL PRIVILEGES ON DATABASE "postgres" FROM "anon"; 2 | REVOKE ALL PRIVILEGES ON DATABASE "postgres" FROM "authenticated"; 3 | 4 | -------------------------------------------------------------------------------- /lib/extractXMLTag.ts: -------------------------------------------------------------------------------- 1 | export function extractXMLTag(xml: string, tag: string) { 2 | const regex = new RegExp(`<${tag}>(.*?)`); 3 | const match = regex.exec(xml); 4 | return match ? match[1] : null; 5 | } 6 | -------------------------------------------------------------------------------- /lib/filesystem/defaultFileSystem.ts: -------------------------------------------------------------------------------- 1 | export const SYSTEM_PATH = "/system"; 2 | export const PROGRAMS_PATH = "/system/programs"; 3 | export const REGISTRY_PATH = "/system/registry.reg"; 4 | export const USER_PATH = "/user"; 5 | -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { login } from "@/lib/auth/actions"; 2 | 3 | export default function LoginPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /state/lastVisitedPath.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | 3 | export const lastVisitedPathAtom = atom(null); 4 | 5 | export function getParentPath(path: string): string { 6 | return path.split("/").slice(0, -1).join("/"); 7 | } 8 | -------------------------------------------------------------------------------- /supabase/migrations/20240711010550_remote_schema.sql: -------------------------------------------------------------------------------- 1 | alter table "public"."generations" enable row level security; 2 | 3 | alter table "public"."tokens" enable row level security; 4 | 5 | alter table "public"."transactions" enable row level security; 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/getSettings.ts: -------------------------------------------------------------------------------- 1 | import { settingsAtom } from "@/state/settings"; 2 | import { getDefaultStore } from "jotai"; 3 | 4 | export function getSettings() { 5 | const store = getDefaultStore(); 6 | const settings = store.get(settingsAtom); 7 | return settings; 8 | } 9 | -------------------------------------------------------------------------------- /supabase/migrations/20240711011456_enable_auth.sql: -------------------------------------------------------------------------------- 1 | -- Disable row level security for the tables 2 | ALTER TABLE public.generations DISABLE ROW LEVEL SECURITY; 3 | ALTER TABLE public.tokens DISABLE ROW LEVEL SECURITY; 4 | ALTER TABLE public.transactions DISABLE ROW LEVEL SECURITY; 5 | 6 | -------------------------------------------------------------------------------- /state/settings.tsx: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from "jotai/utils"; 2 | 3 | export type Settings = { 4 | apiKey: string | null; 5 | model?: "cheap" | "best"; 6 | }; 7 | 8 | export const settingsAtom = atomWithStorage("settings", { 9 | apiKey: null, 10 | model: "best", 11 | }); 12 | -------------------------------------------------------------------------------- /components/ContextMenu.module.css: -------------------------------------------------------------------------------- 1 | .contextMenu { 2 | display: flex; 3 | flex-direction: column; 4 | 5 | & > button { 6 | box-shadow: none; 7 | padding: 4px 16px 4px 16px; 8 | text-align: left; 9 | } 10 | 11 | & > button:hover { 12 | background-color: darkblue; 13 | color: white; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /state/allWindows.tsx: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | import { windowAtomFamily, WindowState } from "./window"; 3 | import { windowsListAtom } from "./windowsList"; 4 | 5 | export const allWindowsAtom = atom((get) => { 6 | const list = get(windowsListAtom); 7 | return list.map((id) => get(windowAtomFamily(id))); 8 | }); 9 | -------------------------------------------------------------------------------- /scripts/lib/scriptClient.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "@/generated/supabase/types"; 2 | import { createClient } from "@supabase/supabase-js"; 3 | 4 | export const createScriptClient = () => { 5 | return createClient( 6 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 7 | process.env.SUPABASE_SERVICE_KEY!, 8 | {} 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /ai/getMaxTokens.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from "@/state/settings"; 2 | import { createClientFromSettings } from "./client"; 3 | 4 | export function getMaxTokens(settings: Settings) { 5 | const { mode } = createClientFromSettings(settings); 6 | if (mode !== "openai" && settings.model === "best") { 7 | return 8192; 8 | } 9 | 10 | return 4000; 11 | } 12 | -------------------------------------------------------------------------------- /components/SettingsLink.tsx: -------------------------------------------------------------------------------- 1 | import { createWindow } from "@/lib/createWindow"; 2 | 3 | export function SettingsLink() { 4 | return ( 5 | { 7 | e.preventDefault(); 8 | createWindow({ title: "Settings", program: { type: "settings" } }); 9 | }} 10 | > 11 | Settings 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | import { appRouter } from "@/server/appRouter"; 3 | 4 | const handler = (req: Request) => 5 | fetchRequestHandler({ 6 | endpoint: "/api/trpc", 7 | req, 8 | router: appRouter, 9 | createContext: () => ({}), 10 | }); 11 | 12 | export { handler as GET, handler as POST }; 13 | -------------------------------------------------------------------------------- /lib/getRegistryKeys.ts: -------------------------------------------------------------------------------- 1 | import { BUILTIN_REGISTRY_KEYS, RegistryEntry } from "@/state/registry"; 2 | 3 | export function getRegistryKeys(registry: RegistryEntry): string[] { 4 | const keys = new Set( 5 | Object.keys(registry).filter((key) => key.startsWith("public_")) 6 | ); 7 | 8 | for (const key of BUILTIN_REGISTRY_KEYS) { 9 | keys.add(key); 10 | } 11 | 12 | return Array.from(keys).sort(); 13 | } 14 | -------------------------------------------------------------------------------- /lib/auth/getUser.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@supabase/supabase-js"; 2 | import { createClient } from "../supabase/server"; 3 | import { isLocal } from "../isLocal"; 4 | 5 | export async function getUser(): Promise { 6 | if (isLocal()) { 7 | return null; 8 | } 9 | const supabase = createClient(); 10 | const { 11 | data: { user }, 12 | } = await supabase.auth.getUser(); 13 | return user; 14 | } 15 | -------------------------------------------------------------------------------- /server/paymentRequiredResponse.ts: -------------------------------------------------------------------------------- 1 | export function createPaymentRequiredResponse() { 2 | return new Response( 3 | JSON.stringify({ 4 | error: "INSUFFICIENT_TOKENS", 5 | message: 6 | "Insufficient tokens. Use a custom key or buy tokens to continue.", 7 | }), 8 | { 9 | status: 402, 10 | headers: { 11 | "Content-Type": "application/json", 12 | }, 13 | } 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /server/usage/getTokens.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@supabase/supabase-js"; 2 | import { Client } from "../../lib/supabase/server"; 3 | 4 | export async function getTokens(client: Client, user: User) { 5 | const token = await client 6 | .from("tokens") 7 | .select("*") 8 | .eq("user_id", user.id) 9 | .single(); 10 | 11 | return { 12 | tokens: (token.data?.free_amount ?? 0) + (token.data?.paid_amount ?? 0), 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /scripts/getEmailForID.ts: -------------------------------------------------------------------------------- 1 | import { createScriptClient } from "./lib/scriptClient"; 2 | 3 | const client = createScriptClient(); 4 | 5 | // Read the ID from command line arguments 6 | const id = process.argv[2]; 7 | 8 | if (!id) { 9 | console.error("Please provide a user ID as a command line argument."); 10 | process.exit(1); 11 | } 12 | 13 | const { data } = await client.auth.admin.getUserById(id); 14 | 15 | console.log(data?.user?.email); 16 | -------------------------------------------------------------------------------- /components/programs/Settings.module.css: -------------------------------------------------------------------------------- 1 | .body { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 4px; 5 | 6 | & section { 7 | display: flex; 8 | flex-direction: column; 9 | gap: 4px; 10 | } 11 | } 12 | 13 | .submit { 14 | margin-left: auto; 15 | margin-top: auto; 16 | } 17 | 18 | .input { 19 | flex-grow: 1; 20 | } 21 | 22 | .label { 23 | white-space: nowrap; 24 | } 25 | 26 | .highlight { 27 | color: green; 28 | } 29 | -------------------------------------------------------------------------------- /lib/filesystem/readFileAsText.ts: -------------------------------------------------------------------------------- 1 | export async function readFileAsText(file: File): Promise { 2 | if (typeof file.text === "function") { 3 | return file.text(); 4 | } else { 5 | return new Promise((resolve, reject) => { 6 | const reader = new FileReader(); 7 | reader.onload = (event) => resolve(event.target?.result as string); 8 | reader.onerror = (error) => reject(error); 9 | reader.readAsText(file); 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/usage/canGenerate.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@supabase/supabase-js"; 2 | 3 | import { Client } from "@/lib/supabase/server"; 4 | import { getTokens } from "./getTokens"; 5 | import { getFlagsForUser } from "@/flags/flags"; 6 | 7 | export async function canGenerate(client: Client, user: User) { 8 | const flags = getFlagsForUser(user); 9 | if (!flags.tokens) { 10 | return true; 11 | } 12 | const { tokens } = await getTokens(client, user); 13 | return tokens > 0; 14 | } 15 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | hostname: "akebmgwfsdwbcjztntxv.supabase.co", 7 | }, 8 | { 9 | hostname: "mynvsgmvogwjsrrm.public.blob.vercel-storage.com", 10 | }, 11 | { 12 | hostname: "localhost", 13 | }, 14 | ], 15 | }, 16 | transpilePackages: ["file-system-access", "fetch-blob"], 17 | }; 18 | 19 | export default nextConfig; 20 | -------------------------------------------------------------------------------- /components/programs/Alert.module.css: -------------------------------------------------------------------------------- 1 | .alertContainer { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | padding: 16px; 6 | } 7 | 8 | .alertContent { 9 | display: flex; 10 | flex-grow: 1; 11 | overflow-y: auto; 12 | } 13 | 14 | .alertIcon { 15 | flex-shrink: 0; 16 | margin-right: 16px; 17 | } 18 | 19 | .alertMessage { 20 | flex-grow: 1; 21 | } 22 | 23 | .alertActions { 24 | display: flex; 25 | justify-content: flex-end; 26 | margin-top: 16px; 27 | gap: 8px; 28 | } 29 | -------------------------------------------------------------------------------- /lib/wrappedFetch.tsx: -------------------------------------------------------------------------------- 1 | import { showUpsell } from "./showUpsell"; 2 | 3 | export async function wrappedFetch( 4 | input: RequestInfo | URL, 5 | init?: RequestInit 6 | ): Promise { 7 | try { 8 | const response = await fetch(input, init); 9 | if (!response.ok && response.status === 402) { 10 | showUpsell(); 11 | } 12 | return response; 13 | } catch (error) { 14 | console.error("Fetch error:", error); 15 | throw error; 16 | } 17 | } 18 | 19 | export default wrappedFetch; 20 | -------------------------------------------------------------------------------- /flags/context.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { DEFAULT_FLAGS, Flags } from "./flags"; 5 | 6 | const FlagsContext = React.createContext(DEFAULT_FLAGS); 7 | 8 | export const useFlags = () => React.useContext(FlagsContext); 9 | 10 | export function FlagsProvider({ 11 | children, 12 | flags, 13 | }: { 14 | children: React.ReactNode; 15 | flags: Flags; 16 | }) { 17 | return ( 18 | {children} 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/error/page.tsx: -------------------------------------------------------------------------------- 1 | export default function ErrorPage() { 2 | return ( 3 |
4 |
5 |
Error
6 |
7 |
8 |

Sorry, something went wrong

9 |
10 | 11 |
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /state/contextMenu.tsx: -------------------------------------------------------------------------------- 1 | import { atom, useSetAtom } from "jotai"; 2 | 3 | export const contextMenuAtom = atom<{ 4 | x: number; 5 | y: number; 6 | items: { label: string; onClick: () => void }[]; 7 | } | null>(null); 8 | 9 | export function useCreateContextMenu() { 10 | const setContextMenu = useSetAtom(contextMenuAtom); 11 | 12 | return (items: { label: string; onClick: () => void }[]) => 13 | (e: React.MouseEvent) => { 14 | e.preventDefault(); 15 | e.stopPropagation(); 16 | setContextMenu({ x: e.clientX, y: e.clientY, items }); 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /lib/actions/ActionsProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | type Actions = { 6 | logout: () => void; 7 | login: () => void; 8 | }; 9 | 10 | const Context = React.createContext({ 11 | logout: () => {}, 12 | login: () => {}, 13 | }); 14 | 15 | export const useActions = () => React.useContext(Context); 16 | 17 | export const ActionsProvider = ({ 18 | children, 19 | actions, 20 | }: { 21 | children: React.ReactNode; 22 | actions: Actions; 23 | }) => { 24 | return {children}; 25 | }; 26 | -------------------------------------------------------------------------------- /lib/filesystem/__test__/filterOutKey.ts: -------------------------------------------------------------------------------- 1 | // Helper function to recursively filter out a key from all objects 2 | export function filterOutKey(obj: any, keyToFilter: string): any { 3 | if (typeof obj !== "object" || obj === null) { 4 | return obj; 5 | } 6 | 7 | if (Array.isArray(obj)) { 8 | return obj.map((item) => filterOutKey(item, keyToFilter)); 9 | } 10 | 11 | return Object.entries(obj).reduce((acc, [key, value]) => { 12 | if (key !== keyToFilter) { 13 | acc[key] = filterOutKey(value, keyToFilter); 14 | } 15 | return acc; 16 | }, {} as any); 17 | } 18 | -------------------------------------------------------------------------------- /components/Window.module.css: -------------------------------------------------------------------------------- 1 | .jiggle { 2 | animation: jiggleAnimation 15s infinite; 3 | } 4 | 5 | @keyframes jiggleAnimation { 6 | 0%, 7 | 100% { 8 | transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); 9 | } 10 | 25% { 11 | transform: rotateX(5deg) rotateY(5deg) rotateZ(5deg); 12 | } 13 | 50% { 14 | transform: rotateX(-5deg) rotateY(-5deg) rotateZ(-5deg); 15 | } 16 | 75% { 17 | transform: rotateX(5deg) rotateY(5deg) rotateZ(5deg); 18 | } 19 | } 20 | 21 | .title { 22 | display: flex; 23 | 24 | flex-direction: row; 25 | align-items: center; 26 | gap: 4px; 27 | } 28 | -------------------------------------------------------------------------------- /flags/flags.ts: -------------------------------------------------------------------------------- 1 | import { config } from "./config"; 2 | 3 | export type Flags = { 4 | [key in keyof typeof config]: boolean; 5 | }; 6 | 7 | export function getFlagsForUser(user: { email?: string } | null): Flags { 8 | const flags: { [key: string]: boolean } = {}; 9 | 10 | for (const [key, value] of Object.entries(config)) { 11 | flags[key] = 12 | (value.allowed?.includes(user?.email ?? "") ?? false) || 13 | value.enabled || 14 | false; 15 | } 16 | 17 | return flags as { [key in keyof typeof config]: boolean }; 18 | } 19 | 20 | export const DEFAULT_FLAGS = getFlagsForUser({}); 21 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 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 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | 39 | .env 40 | .env.test 41 | /public/blob/ 42 | -------------------------------------------------------------------------------- /components/landing/CountDown.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | 5 | export default function CountDown() { 6 | const calculateTimeLeft = () => { 7 | const difference = +new Date(`2024-07-11`) - +new Date(); 8 | let timeLeft = { 9 | days: 0, 10 | }; 11 | 12 | if (difference > 0) { 13 | timeLeft = { 14 | days: Math.ceil(difference / (1000 * 60 * 60 * 24)), 15 | }; 16 | } 17 | 18 | return timeLeft; 19 | }; 20 | 21 | const [timeLeft] = useState(() => calculateTimeLeft()); 22 | 23 | return
{timeLeft.days} days
; 24 | } 25 | -------------------------------------------------------------------------------- /lib/getURLForProgram.ts: -------------------------------------------------------------------------------- 1 | import { ProgramEntry } from "@/state/programs"; 2 | import { RegistryEntry } from "@/state/registry"; 3 | import { getRegistryKeys } from "./getRegistryKeys"; 4 | import { getSettings } from "./getSettings"; 5 | 6 | export function getURLForProgram( 7 | program: ProgramEntry, 8 | registry: RegistryEntry 9 | ) { 10 | const keys = getRegistryKeys(registry); 11 | const keyString = JSON.stringify(keys); 12 | 13 | return `/api/program?description=${program.prompt}&keys=${encodeURIComponent( 14 | keyString 15 | )}&settings=${encodeURIComponent(JSON.stringify(getSettings()))}`; 16 | } 17 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /supabase/migrations/20240714042921_change_token_amount.sql: -------------------------------------------------------------------------------- 1 | -- Modify the tokens table to change the default free_amount 2 | ALTER TABLE public.tokens 3 | ALTER COLUMN free_amount SET DEFAULT 5; 4 | 5 | -- Update the add_weekly_free_tokens function 6 | CREATE OR REPLACE FUNCTION add_weekly_free_tokens() 7 | RETURNS void AS $$ 8 | BEGIN 9 | UPDATE public.tokens 10 | SET free_amount = LEAST(free_amount + 5, 5), 11 | last_weekly_update = CURRENT_TIMESTAMP, 12 | updated_at = CURRENT_TIMESTAMP 13 | WHERE CURRENT_TIMESTAMP - last_weekly_update >= INTERVAL '1 week'; 14 | END; 15 | $$ LANGUAGE plpgsql; 16 | 17 | 18 | -------------------------------------------------------------------------------- /lib/CSPosthogProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import posthog from "posthog-js"; 3 | import { PostHogProvider } from "posthog-js/react"; 4 | import { isLocal } from "@/lib/isLocal"; 5 | 6 | if (typeof window !== "undefined" && !isLocal()) { 7 | posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { 8 | api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, 9 | person_profiles: "identified_only", // or 'always' to create profiles for anonymous users as well 10 | }); 11 | } 12 | 13 | export function CSPostHogProvider({ children }: { children: React.ReactNode }) { 14 | return {children}; 15 | } 16 | -------------------------------------------------------------------------------- /lib/waitForElement.ts: -------------------------------------------------------------------------------- 1 | export async function waitForElement( 2 | id: string, 3 | timeout = 5000 4 | ): Promise { 5 | return new Promise((resolve, reject) => { 6 | const startTime = Date.now(); 7 | 8 | function checkElement() { 9 | const element = document.getElementById(id); 10 | if (element) { 11 | resolve(element); 12 | } else if (Date.now() - startTime > timeout) { 13 | reject(new Error(`Timeout waiting for element with id: ${id}`)); 14 | } else { 15 | requestAnimationFrame(checkElement); 16 | } 17 | } 18 | 19 | requestAnimationFrame(checkElement); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /server/usage/insertGeneration.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@supabase/supabase-js"; 2 | import { Client } from "@/lib/supabase/server"; 3 | import { getFlagsForUser } from "@/flags/flags"; 4 | 5 | export async function insertGeneration({ 6 | client, 7 | user, 8 | tokensUsed, 9 | action, 10 | }: { 11 | client: Client; 12 | user: User; 13 | tokensUsed: number; 14 | action: string; 15 | }) { 16 | const flags = getFlagsForUser(user); 17 | if (!flags.tokens) { 18 | return; 19 | } 20 | await client 21 | .from("generations") 22 | .insert({ 23 | user_id: user.id, 24 | tokens_used: tokensUsed, 25 | action: action, 26 | }) 27 | .single(); 28 | } 29 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "jest"; 2 | import nextJest from "next/jest.js"; 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: "./", 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | const config: Config = { 11 | coverageProvider: "v8", 12 | testEnvironment: "jsdom", 13 | 14 | // Add more setup options before each test is run 15 | // setupFilesAfterEnv: ['/jest.setup.ts'], 16 | }; 17 | 18 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 19 | export default createJestConfig(config); 20 | -------------------------------------------------------------------------------- /state/windowsList.tsx: -------------------------------------------------------------------------------- 1 | import { assertNever } from "@/lib/assertNever"; 2 | import { atomWithReducer } from "jotai/utils"; 3 | 4 | export type WindowsListState = string[]; 5 | 6 | export type WindowsListAction = 7 | | { type: "ADD"; payload: string } 8 | | { type: "REMOVE"; payload: string }; 9 | 10 | export const windowsListAtom = atomWithReducer( 11 | [], 12 | (state: WindowsListState, action: WindowsListAction): WindowsListState => { 13 | switch (action.type) { 14 | case "ADD": 15 | return [...state, action.payload]; 16 | case "REMOVE": 17 | return state.filter((id) => id !== action.payload); 18 | default: 19 | assertNever(action); 20 | } 21 | return state; 22 | } 23 | ); 24 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { OS } from "@/components/OS"; 2 | import { Landing } from "@/components/landing/Landing"; 3 | import { FlagsProvider } from "@/flags/context"; 4 | import { getFlagsForUser } from "@/flags/flags"; 5 | import { ActionsProvider } from "@/lib/actions/ActionsProvider"; 6 | import { login, logout } from "@/lib/auth/actions"; 7 | import { getUser } from "@/lib/auth/getUser"; 8 | import { isLocal } from "@/lib/isLocal"; 9 | 10 | export default async function Home() { 11 | const user = await getUser(); 12 | 13 | return ( 14 | 15 | 16 | {user || isLocal() ? : } 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /lib/put.ts: -------------------------------------------------------------------------------- 1 | import { isLocal } from "./isLocal"; 2 | import { createClient } from "./supabase/server"; 3 | 4 | export async function put(path: string, blob: Blob): Promise { 5 | if (isLocal()) { 6 | const fs = await import("fs-extra"); 7 | 8 | const buffer = await blob.arrayBuffer(); 9 | const data = Buffer.from(buffer); 10 | await fs.outputFile(`${process.cwd()}/public/blob/${path}`, data); 11 | 12 | return `http://localhost:3000/blob/${path}`; 13 | } 14 | const supabase = createClient(); 15 | 16 | const { error } = await supabase.storage.from("icons").upload(path, blob); 17 | 18 | if (error) { 19 | throw error; 20 | } 21 | 22 | return (await supabase.storage.from("icons").getPublicUrl(path)).data 23 | .publicUrl; 24 | } 25 | -------------------------------------------------------------------------------- /app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/lib/supabase/server"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(request: Request) { 5 | const { searchParams, origin } = new URL(request.url); 6 | const code = searchParams.get("code"); 7 | // if "next" is in param, use it as the redirect URL 8 | const next = searchParams.get("next") ?? "/"; 9 | 10 | if (code) { 11 | const supabase = createClient(); 12 | const { error } = await supabase.auth.exchangeCodeForSession(code); 13 | if (!error) { 14 | return NextResponse.redirect(`${origin}${next}`); 15 | } 16 | } 17 | 18 | // return the user to an error page with instructions 19 | return NextResponse.redirect(`${origin}/auth/auth-code-error`); 20 | } 21 | -------------------------------------------------------------------------------- /components/MenuBar.module.css: -------------------------------------------------------------------------------- 1 | .menuBar { 2 | display: flex; 3 | flex-direction: row; 4 | gap: 10px; 5 | } 6 | 7 | .menuBarButton { 8 | border: none; 9 | box-shadow: none !important; 10 | margin: 0; 11 | min-width: 0; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | padding: 0 8px; 16 | 17 | &.isOpen { 18 | background-color: darkblue; 19 | color: #fff; 20 | } 21 | } 22 | 23 | .menuBarButtonContainer { 24 | position: relative; 25 | } 26 | 27 | .menuBarDropdown { 28 | position: absolute; 29 | top: 100%; 30 | left: 0; 31 | 32 | & > button { 33 | box-shadow: none !important; 34 | text-align: left; 35 | } 36 | 37 | & > button:hover { 38 | background-color: darkblue; 39 | color: white; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/api/APIProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { httpBatchLink } from "@trpc/client"; 5 | import React, { useState } from "react"; 6 | import { trpc } from "./client"; 7 | 8 | export function APIProvider({ children }: { children: React.ReactNode }) { 9 | const [queryClient] = useState(() => new QueryClient()); 10 | const [trpcClient] = useState(() => 11 | trpc.createClient({ 12 | links: [ 13 | httpBatchLink({ 14 | url: "/api/trpc", 15 | }), 16 | ], 17 | }) 18 | ); 19 | 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /server/usage/createTransaction.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@/lib/supabase/server"; 2 | 3 | export async function createTransaction({ 4 | client, 5 | userId, 6 | amount, 7 | tokensPurchased, 8 | }: { 9 | client: Client; 10 | userId: string; 11 | amount: number; 12 | tokensPurchased: number; 13 | }): Promise<{ success: boolean; error?: string }> { 14 | try { 15 | const { error } = await client.from("transactions").insert({ 16 | user_id: userId, 17 | amount: amount, 18 | tokens_purchased: tokensPurchased, 19 | }); 20 | 21 | if (error) { 22 | throw error; 23 | } 24 | 25 | return { success: true }; 26 | } catch (error) { 27 | console.error("Error creating transaction:", error); 28 | return { success: false, error: "Failed to create transaction" }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { updateSession } from "@/lib/supabase/middleware"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | import { isLocal } from "./lib/isLocal"; 4 | 5 | export async function middleware(request: NextRequest) { 6 | if (isLocal()) { 7 | return NextResponse.next(); 8 | } 9 | // update user's auth session 10 | return await updateSession(request); 11 | } 12 | 13 | export const config = { 14 | matcher: [ 15 | /* 16 | * Match all request paths except for the ones starting with: 17 | * - _next/static (static files) 18 | * - _next/image (image optimization files) 19 | * - favicon.ico (favicon file) 20 | * Feel free to modify this pattern to include more paths. 21 | */ 22 | "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /server/appRouter.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/lib/supabase/server"; 2 | import { publicProcedure, router } from "./trpc"; 3 | import { getUser } from "@/lib/auth/getUser"; 4 | import { TRPCError } from "@trpc/server"; 5 | import { getTokens } from "./usage/getTokens"; 6 | import { isLocal } from "@/lib/isLocal"; 7 | 8 | export const appRouter = router({ 9 | getTokens: publicProcedure.query(async () => { 10 | if (isLocal()) { 11 | return { 12 | tokens: 1000, 13 | }; 14 | } 15 | const client = createClient(); 16 | const user = await getUser(); 17 | if (!user) { 18 | throw new TRPCError({ 19 | code: "UNAUTHORIZED", 20 | message: "Unauthorized", 21 | }); 22 | } 23 | return await getTokens(client, user); 24 | }), 25 | }); 26 | 27 | export type AppRouter = typeof appRouter; 28 | -------------------------------------------------------------------------------- /app/api/checkout/route.ts: -------------------------------------------------------------------------------- 1 | import { getUser } from "@/lib/auth/getUser"; 2 | import { redirect } from "next/navigation"; 3 | 4 | import Stripe from "stripe"; 5 | 6 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); 7 | 8 | export async function POST(): Promise { 9 | const user = await getUser(); 10 | if (!user) { 11 | return new Response("Unauthorized", { status: 401 }); 12 | } 13 | const session = await stripe.checkout.sessions.create({ 14 | line_items: [ 15 | { 16 | price: process.env.STRIPE_PRICE_ID, 17 | quantity: 1, 18 | }, 19 | ], 20 | mode: "payment", 21 | success_url: `${process.env.NEXT_PUBLIC_APP_URL}`, 22 | cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}`, 23 | metadata: { 24 | userId: user.id, 25 | }, 26 | }); 27 | 28 | return redirect(session.url!); 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - uses: oven-sh/setup-bun@v2 17 | with: 18 | bun-version: latest 19 | 20 | - name: Use Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: latest 24 | 25 | - name: Install dependencies 26 | run: bun install 27 | 28 | - name: Lint 29 | run: bun run lint 30 | 31 | - name: Type check 32 | run: npx tsc --noEmit 33 | 34 | - name: Test 35 | run: bun run test 36 | 37 | - name: Build 38 | run: bun run build 39 | 40 | - name: Build iframe 41 | run: bun run build-iframe 42 | -------------------------------------------------------------------------------- /state/registry.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | import { REGISTRY_PATH } from "@/lib/filesystem/defaultFileSystem"; 3 | import { getFsManager } from "@/state/fsManager"; 4 | 5 | export interface RegistryEntry { 6 | [key: string]: any; 7 | } 8 | 9 | export const registryAtom = atom( 10 | async (get) => { 11 | const fs = await getFsManager(); 12 | const registry = await get(fs.getFileAtom(REGISTRY_PATH)); 13 | try { 14 | return JSON.parse((registry?.content as string) || "{}"); 15 | } catch (error) { 16 | console.error("Failed to parse registry:", error); 17 | return {}; 18 | } 19 | }, 20 | async (_get, _set, update: RegistryEntry) => { 21 | const fs = await getFsManager(); 22 | await fs.writeFile(REGISTRY_PATH, JSON.stringify(update)); 23 | } 24 | ); 25 | 26 | export const DESKTOP_URL_KEY = "public_desktop_url"; 27 | 28 | export const BUILTIN_REGISTRY_KEYS = [DESKTOP_URL_KEY]; 29 | -------------------------------------------------------------------------------- /lib/auth/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import { redirect } from "next/navigation"; 5 | 6 | import { createClient } from "@/lib/supabase/server"; 7 | 8 | export async function login() { 9 | const supabase = createClient(); 10 | 11 | const url = `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`; 12 | 13 | const { data, error } = await supabase.auth.signInWithOAuth({ 14 | provider: "google", 15 | options: { 16 | redirectTo: url, 17 | }, 18 | }); 19 | 20 | if (error) { 21 | redirect("/error"); 22 | } 23 | 24 | revalidatePath("/", "layout"); 25 | 26 | if (data.url) { 27 | redirect(data.url); // use the redirect API for your server framework 28 | } 29 | } 30 | 31 | export async function logout() { 32 | const supabase = createClient(); 33 | 34 | await supabase.auth.signOut(); 35 | revalidatePath("/", "layout"); 36 | redirect("/"); 37 | } 38 | -------------------------------------------------------------------------------- /lib/getSettingsFromRequest.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from "@/state/settings"; 2 | 3 | // TODO check if this works 4 | export async function getSettingsFromGetRequest( 5 | req: Request 6 | ): Promise { 7 | const url = new URL(req.url); 8 | const settingsParam = url.searchParams.get("settings"); 9 | if (settingsParam) { 10 | try { 11 | return JSON.parse(decodeURIComponent(settingsParam)); 12 | } catch (error) { 13 | console.error("Error parsing settings from query string:", error); 14 | return { apiKey: null }; 15 | } 16 | } 17 | return { apiKey: null }; 18 | } 19 | 20 | export async function getSettingsFromJSON(json: any): Promise { 21 | const settings = json.settings; 22 | 23 | if (!settings) { 24 | return { apiKey: null, model: "best" }; 25 | } 26 | return { 27 | apiKey: settings.apiKey, 28 | model: settings.model === "cheap" ? "cheap" : "best", 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext", 7 | "dom.asynciterable" 8 | ], 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "allowJs": true, 12 | "skipLibCheck": true, 13 | "strict": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "bundler", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "incremental": true, 22 | "plugins": [ 23 | { 24 | "name": "next" 25 | } 26 | ], 27 | "paths": { 28 | "@/*": [ 29 | "./*" 30 | ] 31 | }, 32 | "target": "ESNext" 33 | }, 34 | "include": [ 35 | "next-env.d.ts", 36 | "**/*.ts", 37 | "**/*.tsx", 38 | ".next/types/**/*.ts" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /ai/image.ts: -------------------------------------------------------------------------------- 1 | import { log } from "@/lib/log"; 2 | import Replicate from "replicate"; 3 | import sharp from "sharp"; 4 | 5 | export async function generateIcon(prompt: string): Promise { 6 | const replicate = new Replicate(); 7 | 8 | const response = await replicate.run( 9 | "fofr/sticker-maker:4acb778eb059772225ec213948f0660867b2e03f277448f18cf1800b96a65a1a", 10 | { 11 | input: { 12 | prompt, 13 | }, 14 | } 15 | ); 16 | 17 | log(response); 18 | 19 | const url = Array.isArray(response) ? response[0] : null; 20 | 21 | if (!url) return null; 22 | 23 | const arrayBuffer = await fetch(url).then((res) => res.arrayBuffer()); 24 | 25 | // Make it crunchy 26 | const processedImageBuffer = await sharp(arrayBuffer) 27 | .resize(48, 48) 28 | .webp({ quality: 80 }) 29 | .toBuffer(); 30 | 31 | const blob = new Blob([processedImageBuffer], { type: "image/webp" }); 32 | 33 | return blob; 34 | } 35 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "98.css"; 4 | import "./globals.css"; 5 | import { CSPostHogProvider } from "@/lib/CSPosthogProvider"; 6 | import { APIProvider } from "@/lib/api/APIProvider"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Windows 9X", 12 | description: "The future of yesterday", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 26 | 27 | 28 | {children} 29 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /lib/supabase/server.ts: -------------------------------------------------------------------------------- 1 | import { Database } from "@/generated/supabase/types"; 2 | import { createServerClient } from "@supabase/ssr"; 3 | import { cookies } from "next/headers"; 4 | 5 | export function createClient() { 6 | const cookieStore = cookies(); 7 | 8 | return createServerClient( 9 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 10 | process.env.SUPABASE_SERVICE_KEY!, 11 | { 12 | cookies: { 13 | getAll() { 14 | return cookieStore.getAll(); 15 | }, 16 | setAll(cookiesToSet) { 17 | try { 18 | cookiesToSet.forEach(({ name, value, options }) => 19 | cookieStore.set(name, value, options) 20 | ); 21 | } catch { 22 | // The `setAll` method was called from a Server Component. 23 | // This can be ignored if you have middleware refreshing 24 | // user sessions. 25 | } 26 | }, 27 | }, 28 | } 29 | ); 30 | } 31 | 32 | export type Client = ReturnType; 33 | -------------------------------------------------------------------------------- /components/landing/Form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useFormState } from "react-dom"; 3 | 4 | export type SignUpState = 5 | | { type: "error"; error: string } 6 | | { type: "success" } 7 | | { type: "initial" }; 8 | 9 | const initialState: SignUpState = { type: "initial" }; 10 | 11 | export function Form({ 12 | action, 13 | }: { 14 | action: (prevState: SignUpState, formData: FormData) => Promise; 15 | }) { 16 | const [state, formAction] = useFormState(action, initialState); 17 | return ( 18 |
19 |
20 | 21 | 22 |
23 | 24 | {state.type === "error" &&

{state.error}

} 25 | {state.type === "success" && ( 26 |

27 | We'll let you know when we launch! 28 |

29 | )} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /components/programs/History.module.css: -------------------------------------------------------------------------------- 1 | .historyContainer { 2 | background-color: #c0c0c0; 3 | border: 2px solid; 4 | border-top-color: #dfdfdf; 5 | border-left-color: #dfdfdf; 6 | border-right-color: #808080; 7 | border-bottom-color: #808080; 8 | box-shadow: 1px 1px 0 0 #000000; 9 | padding: 2px; 10 | } 11 | 12 | .versionList { 13 | background-color: #ffffff; 14 | border: 1px solid #808080; 15 | height: 200px; 16 | overflow-y: auto; 17 | } 18 | 19 | .versionItem { 20 | padding: 4px 8px; 21 | cursor: pointer; 22 | font-family: "MS Sans Serif", Arial, sans-serif; 23 | font-size: 11px; 24 | display: flex; 25 | } 26 | 27 | .versionItem:hover { 28 | background-color: #000080; 29 | color: white; 30 | } 31 | 32 | .versionItem.current { 33 | background-color: #dfdfdf; 34 | } 35 | 36 | .versionItem.current:hover { 37 | background-color: #000080; 38 | color: white; 39 | } 40 | 41 | .versionDate { 42 | display: inline-block; 43 | width: 100%; 44 | } 45 | 46 | .currentLabel { 47 | margin-left: 8px; 48 | font-style: italic; 49 | } 50 | -------------------------------------------------------------------------------- /components/programs/Explorer.module.css: -------------------------------------------------------------------------------- 1 | .explorer { 2 | width: 100%; 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | gap: 4px; 7 | 8 | & table { 9 | width: 100%; 10 | } 11 | } 12 | 13 | .tableWrapper { 14 | height: 100%; 15 | } 16 | 17 | .pathBar { 18 | display: flex; 19 | align-items: center; 20 | gap: 4px; 21 | 22 | & input { 23 | width: 100%; 24 | } 25 | } 26 | 27 | .saveSection { 28 | display: flex; 29 | align-items: center; 30 | gap: 4px; 31 | 32 | & label { 33 | text-wrap: nowrap; 34 | } 35 | 36 | & input { 37 | width: 100%; 38 | } 39 | } 40 | 41 | .actions { 42 | display: flex; 43 | align-items: center; 44 | gap: 4px; 45 | 46 | & button { 47 | padding: 4px; 48 | min-width: inherit; 49 | box-shadow: none; 50 | border: none; 51 | cursor: pointer; 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | gap: 4px; 56 | flex-direction: column; 57 | 58 | & img { 59 | width: 24px; 60 | height: 24px; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/capture.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/lib/supabase/server"; 2 | import { PostHog } from "posthog-node"; 3 | import { isLocal } from "./isLocal"; 4 | 5 | type Event = { 6 | type: "chat" | "icon" | "name" | "program" | "help"; 7 | usedOwnKey: boolean; 8 | model: string; 9 | }; 10 | 11 | export async function capture(event: Event, req: Request) { 12 | if (isLocal()) { 13 | return; 14 | } 15 | 16 | const supabase = createClient(); 17 | const user = await supabase.auth.getUser(); 18 | const { type, ...props } = event; 19 | const posthog = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, { 20 | host: process.env.NEXT_PUBLIC_POSTHOG_HOST!, 21 | }); 22 | 23 | const ip = 24 | req.headers.get("x-forwarded-for") || 25 | req.headers.get("x-real-ip") || 26 | "unknown"; 27 | const country = req.headers.get("X-Vercel-IP-Country") || "unknown"; 28 | 29 | posthog.capture({ 30 | event: type, 31 | properties: { 32 | ...props, 33 | ip, 34 | country, 35 | }, 36 | distinctId: user.data.user?.id ?? "null", 37 | }); 38 | await posthog.shutdown(); 39 | } 40 | -------------------------------------------------------------------------------- /lib/initState.ts: -------------------------------------------------------------------------------- 1 | import { WIDTH } from "@/components/programs/Welcome"; 2 | import { createWindow } from "./createWindow"; 3 | import { isMobile } from "./isMobile"; 4 | import { waitForElement } from "./waitForElement"; 5 | 6 | let initialized = false; 7 | 8 | export function initState() { 9 | if (initialized) return; 10 | initialized = true; 11 | const id = createWindow({ 12 | title: "Welcome", 13 | program: { type: "welcome" }, 14 | 15 | size: { width: WIDTH, height: "auto" }, 16 | }); 17 | if (!isMobile()) { 18 | waitForElement(id).then((el) => { 19 | if (el) { 20 | const welcomeRect = el.getBoundingClientRect(); 21 | const runWidth = 200; 22 | const runLeft = welcomeRect.left - 100; // Overlap by 50 pixels 23 | const runTop = welcomeRect.top + 200; // Offset slightly from the top of Welcome 24 | 25 | createWindow({ 26 | title: "Run", 27 | program: { type: "run" }, 28 | size: { width: runWidth, height: "auto" }, 29 | pos: { x: runLeft, y: runTop }, 30 | }); 31 | } 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/runProgramFromPath.tsx: -------------------------------------------------------------------------------- 1 | import { createWindow } from "@/lib/createWindow"; 2 | import { getFsManager } from "@/state/fsManager"; 3 | 4 | export async function runProgramFromPath(exePath: string): Promise { 5 | try { 6 | const fsManager = await getFsManager(); 7 | const exeFile = await fsManager.getFile(exePath, "deep"); 8 | 9 | if (!exeFile || exeFile.type !== "file") { 10 | throw new Error("Invalid exe file path"); 11 | } 12 | 13 | const exeContent = exeFile.content as string; 14 | const config = JSON.parse(exeContent); 15 | 16 | // Get the parent folder name 17 | const parentFolderName = exePath.split("/").slice(-2)[0]; 18 | 19 | // Create a window for the program 20 | createWindow({ 21 | title: parentFolderName, 22 | program: { 23 | type: "iframe", 24 | programID: parentFolderName, 25 | }, 26 | icon: config.icon ?? undefined, 27 | }); 28 | 29 | console.log("Running program:", parentFolderName); 30 | } catch (error) { 31 | console.error("Error running program:", error); 32 | throw error; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/alert.ts: -------------------------------------------------------------------------------- 1 | import { createWindow } from "@/lib/createWindow"; 2 | import { getDefaultStore } from "jotai"; 3 | import { AlertAction, WindowState } from "@/state/window"; 4 | import { allWindowsAtom } from "@/state/allWindows"; 5 | import { ReactNode } from "react"; 6 | 7 | type AlertOptions = { 8 | message: ReactNode; 9 | alertId?: string; 10 | icon?: "x"; 11 | actions?: AlertAction[]; 12 | }; 13 | 14 | export function alert({ message, alertId, icon, actions }: AlertOptions) { 15 | const store = getDefaultStore(); 16 | const existingWindows = store.get(allWindowsAtom); 17 | 18 | // Check if an alert with the same alertId already exists 19 | if ( 20 | alertId && 21 | existingWindows.some( 22 | (w: WindowState) => 23 | w.program.type === "alert" && w.program.alertId === alertId 24 | ) 25 | ) { 26 | return; 27 | } 28 | 29 | createWindow({ 30 | title: "Alert", 31 | size: { 32 | width: 400, 33 | height: "auto", 34 | }, 35 | program: { 36 | type: "alert", 37 | message, 38 | alertId, 39 | icon, 40 | actions, 41 | }, 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /lib/debounce.ts: -------------------------------------------------------------------------------- 1 | export function debounce void>( 2 | func: T, 3 | delay: number 4 | ): (...args: Parameters) => void { 5 | let timeoutId: ReturnType | null = null; 6 | 7 | return function (this: any, ...args: Parameters) { 8 | if (timeoutId) { 9 | clearTimeout(timeoutId); 10 | } 11 | 12 | timeoutId = setTimeout(() => { 13 | func.apply(this, args); 14 | }, delay); 15 | }; 16 | } 17 | 18 | export function throttle void>( 19 | func: T, 20 | limit: number 21 | ): (...args: Parameters) => void { 22 | let lastFunc: ReturnType; 23 | let lastRan: number; 24 | 25 | return function (this: any, ...args: Parameters) { 26 | if (!lastRan) { 27 | func.apply(this, args); 28 | lastRan = Date.now(); 29 | } else { 30 | clearTimeout(lastFunc); 31 | lastFunc = setTimeout(() => { 32 | if (Date.now() - lastRan >= limit) { 33 | func.apply(this, args); 34 | lastRan = Date.now(); 35 | } 36 | }, limit - (Date.now() - lastRan)); 37 | } 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | html, 8 | body { 9 | max-width: 100vw; 10 | overflow-x: hidden; 11 | user-select: none; 12 | font-family: "Pixelated MS Sans Serif", Arial, sans-serif; 13 | touch-action: none; 14 | 15 | & .title-bar, 16 | & .window, 17 | & button, 18 | & input, 19 | & label, 20 | & option, 21 | & select, 22 | & table, 23 | & textarea, 24 | & ul.tree-view { 25 | -webkit-font-smoothing: inherit !important; 26 | } 27 | } 28 | 29 | a { 30 | /* color: inherit; */ 31 | text-decoration: none; 32 | } 33 | 34 | :root { 35 | --taskbar-height: 32px; 36 | } 37 | 38 | ::-webkit-scrollbar-button:vertical:start:increment, 39 | ::-webkit-scrollbar-button:vertical:end:decrement, 40 | ::-webkit-scrollbar-button:horizontal:start:increment, 41 | ::-webkit-scrollbar-button:horizontal:end:decrement { 42 | display: none; 43 | } 44 | 45 | h1 { 46 | font-size: 2rem; 47 | } 48 | 49 | h2 { 50 | font-size: 1.5rem; 51 | } 52 | 53 | h3 { 54 | font-size: 1.25rem; 55 | } 56 | 57 | h4 { 58 | font-size: 1rem; 59 | } 60 | 61 | h5 { 62 | font-size: 0.75rem; 63 | } 64 | -------------------------------------------------------------------------------- /components/ContextMenu.tsx: -------------------------------------------------------------------------------- 1 | import { contextMenuAtom } from "@/state/contextMenu"; 2 | import { useAtom } from "jotai"; 3 | import { useEffect } from "react"; 4 | import styles from "./ContextMenu.module.css"; 5 | 6 | export function ContextMenu() { 7 | const [contextMenu, setContextMenu] = useAtom(contextMenuAtom); 8 | 9 | useEffect(() => { 10 | const handleClick = () => { 11 | setContextMenu(null); 12 | }; 13 | 14 | window.addEventListener("click", handleClick); 15 | 16 | return () => { 17 | window.removeEventListener("click", handleClick); 18 | }; 19 | }, [setContextMenu]); 20 | 21 | if (!contextMenu) return null; 22 | 23 | const { x, y, items } = contextMenu; 24 | 25 | return ( 26 |
35 |
36 | {items.map((item, index) => ( 37 | 40 | ))} 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /components/WindowBody.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { assertNever } from "@/lib/assertNever"; 4 | import { WindowState } from "@/state/window"; 5 | import { Iframe } from "./programs/Iframe"; 6 | import { Welcome } from "./programs/Welcome"; 7 | import { Run } from "./programs/Run"; 8 | import { Help } from "./programs/Help"; 9 | import { Explorer } from "./programs/Explorer"; 10 | import { Settings } from "./programs/Settings"; 11 | import { History } from "./programs/History"; 12 | import { Alert } from "./programs/Alert"; 13 | 14 | export function WindowBody({ state }: { state: WindowState }) { 15 | switch (state.program.type) { 16 | case "welcome": 17 | return ; 18 | case "run": 19 | return ; 20 | case "iframe": 21 | return