Skip to content

Commit

Permalink
feat(hub): actor repl
Browse files Browse the repository at this point in the history
  • Loading branch information
jog1t committed Jan 18, 2025
1 parent 52b8aad commit 595cdfd
Show file tree
Hide file tree
Showing 17 changed files with 842 additions and 223 deletions.
8 changes: 5 additions & 3 deletions frontend/apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@hookform/resolvers": "^3.3.4",
"@rivet-gg/actor-protocol": "workspace:*",
"@rivet-gg/actor-client": "*",
"@rivet-gg/actor-protocol": "*",
"@rivet-gg/api": "file:vendor/rivet-gg-api.tgz",
"@rivet-gg/api-ee": "file:vendor/rivet-gg-api-ee.tgz",
"@rivet-gg/components": "workspace:*",
Expand All @@ -31,12 +32,13 @@
"@tanstack/router-zod-adapter": "^1.81.5",
"@types/bcryptjs": "^2.4.6",
"bcryptjs": "^2.4.3",
"esast-util-from-js": "^2.0.1",
"file-saver": "^2.0.5",
"framer-motion": "^11.2.11",
"lodash": "^4.17.21",
"posthog-js": "^1.144.2",
"react": "*",
"react-dom": "*",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.51.1",
"react-konami-code": "^2.3.0",
"react-turnstile": "^1.1.3",
Expand Down
91 changes: 91 additions & 0 deletions frontend/apps/hub/src/components/repl/repl-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
CodeMirror,
type CodeMirrorRef,
type CompletionContext,
EditorView,
External,
defaultKeymap,
javascript,
javascriptLanguage,
keymap,
} from "@rivet-gg/components/code-mirror";
import { forwardRef } from "react";

const deleteBgTheme = EditorView.theme({
".cm-content": { padding: 0 },
});

interface ReplInputProps {
rpcs: string[];
onRun: (code: string) => void;
}

export const ReplInput = forwardRef<CodeMirrorRef, ReplInputProps>(
({ rpcs, onRun }, ref) => {
const rivetKeymap = keymap.of([
{
key: "Shift-Enter",
run: (editor) => {
onRun(editor?.state.doc.toString());
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: "",
},
annotations: [External.of(true)],
});
return true;
},
},
...defaultKeymap,
]);

const replAutocomplete = javascriptLanguage.data.of({
autocomplete: (context: CompletionContext) => {
const word = context.matchBefore(/^\w*/);
if (!word || (word?.from === word?.to && !context.explicit))
return null;
return {
from: word.from,
to: word.to,
boost: 99,
options: [
{
label: "wait",
apply: "wait",
validFor: /^(@\w*)?$/,
info: "Helper function to wait for a number of milliseconds",
},
...rpcs.map((rpc) => ({
label: rpc,
apply: rpc,
validFor: /^(@\w*)?$/,
info: `Call "${rpc}" RPC on Actor`,
})),
],
};
},
});

return (
<CodeMirror
autoFocus
basicSetup={{
lineNumbers: false,
lintKeymap: false,
foldKeymap: false,
searchKeymap: false,
defaultKeymap: false,
foldGutter: false,
}}
extensions={[
deleteBgTheme,
rivetKeymap,
javascript(),
replAutocomplete,
]}
/>
);
},
);
121 changes: 121 additions & 0 deletions frontend/apps/hub/src/components/repl/repl-log.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { cn } from "@rivet-gg/components";
import {
Icon,
faArrowLeft,
faArrowRight,
faCircleX,
faSpinner,
} from "@rivet-gg/icons";
import type { ReplCommand } from "./repl-state";

interface ReplLogProps {
commands: ReplCommand[];
}

export function ReplLog({ commands }: ReplLogProps) {
return (
<div className="flex flex-col gap-3">
{commands.map((command) => (
<div key={command.key} className="flex flex-col gap-1">
<div className="text-xs font-mono text-muted-foreground flex gap-1">
<div className="min-w-4 text-center">
<Icon icon={faArrowRight} />
</div>

{command.status === "pending" ? (
<div className="text-muted-foreground text-xs font-mono">
...
</div>
) : null}

{command.status !== "pending" && command.formatted ? (
<div
className="text-xs font-mono text-muted-foreground"
style={{ color: command.formatted.fg }}
>
{command.formatted?.tokens.map(
(tokensLine, index) => (
<span
// biome-ignore lint/suspicious/noArrayIndexKey: we're using the index as a key here because the array is static
key={index}
className="block"
>
{tokensLine.map((token, index) => (
<span
// biome-ignore lint/suspicious/noArrayIndexKey: we're using the index as a key here because the array is static
key={index}
style={{
color: token.color,
}}
className="whitespace-pre"
>
{token.content}
</span>
))}
</span>
),
)}
</div>
) : null}
</div>
<div className="flex flex-col gap-1 pl-4 max-w-full min-w-0">
{command.logs?.map((log, index) => (
<div
// biome-ignore lint/suspicious/noArrayIndexKey: we're using the index as a key here because the array is static
key={index}
className={cn(
"text-xs font-mono break-words min-w-0 max-w-100",
{
"text-muted-destructive":
log.level === "error",
},
)}
>
{log.message}
</div>
))}
</div>
{command.status !== "success" &&
command.status !== "error" ? (
<div className="text-xs font-mono text-muted-foreground flex py-0.5 gap-1">
<div className="min-w-4 text-center">
<Icon
icon={faSpinner}
className="animate-spin"
/>
</div>
Waiting for response...
</div>
) : null}
{command.status === "success" ? (
<div className="text-xs font-mono text-muted-foreground flex py-0.5 gap-1 w-full">
<div className="min-w-4 text-center">
<Icon icon={faArrowLeft} />
</div>

<div className="break-words min-w-0 max-w-full">
{(command.result as string) || "undefined"}
</div>
</div>
) : null}
{command.status === "error" ? (
<div className="text-xs font-mono bg-muted-destructive/50 flex py-0.5 gap-1">
<div className="min-w-4 text-center">
<Icon icon={faCircleX} />
</div>

<div className="break-words">
{typeof command.error === "object" &&
command.error &&
"message" in command.error
? (command.error.message as string) ||
"undefined"
: "undefined"}
</div>
</div>
) : null}
</div>
))}
</div>
);
}
57 changes: 57 additions & 0 deletions frontend/apps/hub/src/components/repl/repl-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { z } from "zod";

export const MessageSchema = z.object({
type: z.literal("code"),
data: z.string(),
managerUrl: z.string(),
actorId: z.string(),
rpcs: z.array(z.string()),
id: z.string(),
});

export const FormattedCodeSchema = z
.object({
fg: z.string(),
tokens: z.array(
z.array(
z.object({
content: z.string(),
color: z.string(),
}),
),
),
})
.catch((ctx) => ctx.input);

export const LogSchema = z.object({
level: z.string(),
message: z.any(),
});

export const ResponseSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("error"),
id: z.string(),
data: z.any(),
}),
z.object({
type: z.literal("formatted"),
id: z.string(),
data: FormattedCodeSchema,
}),
z.object({
type: z.literal("result"),
id: z.string(),
data: z.any(),
}),
z.object({
type: z.literal("log"),
id: z.string(),
data: LogSchema,
}),
]);

export type Response = z.infer<typeof ResponseSchema>;
export type Message = z.infer<typeof MessageSchema>;
export type FormattedCode = z.infer<typeof FormattedCodeSchema>;
export type Log = z.infer<typeof LogSchema>;
Loading

0 comments on commit 595cdfd

Please sign in to comment.