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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added packages/app/pr/screenshots/minimap-rail-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/app/pr/screenshots/minimap-rail-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/app/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2002,6 +2002,7 @@ export default function App() {
listAgents={listAgents}
setSessionAgent={setSessionAgent}
saveSession={saveSessionExport}
sessionStatusById={activeSessionStatusById()}
onTryNotionPrompt={() => {
setPrompt("setup my crm");
setTryNotionPromptVisible(false);
Expand Down
50 changes: 50 additions & 0 deletions packages/app/src/app/components/flyout-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Show, createSignal, onMount } from "solid-js";
import { Check, FileText, Folder } from "lucide-solid";

export type FlyoutProps = {
item: {
id: string;
rect: { top: number; left: number; width: number; height: number };
targetRect: { top: number; left: number; width: number; height: number };
label: string;
icon: "file" | "check" | "folder";
};
};

export default function FlyoutItem(props: FlyoutProps) {
const [active, setActive] = createSignal(false);
onMount(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setActive(true);
});
});
});

return (
<div
class="fixed z-[100] pointer-events-none transition-all duration-1000 ease-[cubic-bezier(0.16,1,0.3,1)] flex items-center gap-2 px-3 py-2 rounded-xl bg-gray-12 text-gray-1 shadow-xl border border-gray-11/20"
style={{
top: `${props.item.rect.top}px`,
left: `${props.item.rect.left}px`,
transform: active()
? `translate(${props.item.targetRect.left - props.item.rect.left}px, ${
props.item.targetRect.top - props.item.rect.top
}px) scale(0.3)`
: "translate(0, 0) scale(1)",
opacity: active() ? 0 : 1,
}}
>
<Show when={props.item.icon === "check"}>
<Check size={14} />
</Show>
<Show when={props.item.icon === "file"}>
<FileText size={14} />
</Show>
<Show when={props.item.icon === "folder"}>
<Folder size={14} />
</Show>
<span class="text-xs font-medium truncate max-w-[120px]">{props.item.label}</span>
</div>
);
}
209 changes: 209 additions & 0 deletions packages/app/src/app/components/session/composer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
import { ArrowRight, Zap } from "lucide-solid";

export type CommandItem = {
id: string;
description: string;
};

export type ComposerProps = {
prompt: string;
setPrompt: (value: string) => void;
busy: boolean;
onSend: () => void;
commandMatches: CommandItem[];
onRunCommand: (commandId: string) => void;
selectedModelLabel: string;
onModelClick: () => void;
showNotionBanner: boolean;
onNotionBannerClick: () => void;
toast: string | null;
};

export default function Composer(props: ComposerProps) {
let textareaRef: HTMLTextAreaElement | undefined;
const [commandIndex, setCommandIndex] = createSignal(0);

const commandMenuOpen = createMemo(() => {
return props.prompt.startsWith("/") && !props.busy;
});

const syncHeight = () => {
if (!textareaRef) return;
textareaRef.style.height = "auto";
const nextHeight = Math.min(textareaRef.scrollHeight, 160);
textareaRef.style.height = `${nextHeight}px`;
textareaRef.style.overflowY = textareaRef.scrollHeight > 160 ? "auto" : "hidden";
};

createEffect(() => {
props.prompt;
syncHeight();
});

createEffect(() => {
if (commandMenuOpen()) {
setCommandIndex(0);
}
});

const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Enter" && event.shiftKey) return;
if (event.isComposing && event.key !== "Enter") return;

if (commandMenuOpen()) {
const matches = props.commandMatches;
if (event.key === "Enter") {
event.preventDefault();
const active = matches[commandIndex()] ?? matches[0];
if (active) {
props.onRunCommand(active.id);
}
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
setCommandIndex((i) => Math.min(i + 1, matches.length - 1));
return;
}
if (event.key === "ArrowUp") {
event.preventDefault();
setCommandIndex((i) => Math.max(i - 1, 0));
return;
}
if (event.key === "Escape") {
event.preventDefault();
props.setPrompt("");
return;
}
if (event.key === "Tab") {
event.preventDefault();
// maybe autocomplete?
const active = matches[commandIndex()] ?? matches[0];
if (active) {
props.onRunCommand(active.id);
}
return;
}
}

if (event.key === "Enter") {
event.preventDefault();
props.onSend();
}
};

createEffect(() => {
const handler = () => {
textareaRef?.focus();
};
window.addEventListener("openwork:focusPrompt", handler);
onCleanup(() => window.removeEventListener("openwork:focusPrompt", handler));
});

return (
<div class="p-4 border-t border-gray-6 bg-gray-1 sticky bottom-0 z-20">
<div class="max-w-2xl mx-auto">
<div
class={`bg-gray-2 border border-gray-6 rounded-3xl overflow-visible transition-all shadow-2xl relative group/input ${
commandMenuOpen()
? "rounded-t-none border-t-transparent"
: "focus-within:ring-1 focus-within:ring-gray-7"
}`}
>
<Show when={commandMenuOpen()}>
<div class="absolute bottom-full left-[-1px] right-[-1px] z-30">
<div class="rounded-t-3xl border border-gray-6 border-b-0 bg-gray-2 shadow-2xl overflow-hidden">
<div class="px-4 pt-3 pb-2 text-[10px] font-semibold uppercase tracking-[0.2em] text-gray-8 border-b border-gray-6/30 bg-gray-2">
Commands
</div>
<div class="space-y-1 p-2 bg-gray-2">
<Show
when={props.commandMatches.length}
fallback={
<div class="px-3 py-2 text-xs text-gray-9">No commands found.</div>
}
>
<For each={props.commandMatches}>
{(command, idx) => (
<button
type="button"
class={`w-full flex items-start gap-3 rounded-xl px-3 py-2 text-left transition-colors ${
idx() === commandIndex()
? "bg-gray-12/10 text-gray-12"
: "text-gray-11 hover:bg-gray-12/5"
}`}
onMouseDown={(e) => {
e.preventDefault();
props.onRunCommand(command.id);
}}
onMouseEnter={() => setCommandIndex(idx())}
>
<div class="text-xs font-semibold text-gray-12">/{command.id}</div>
<div class="text-[11px] text-gray-9">{command.description}</div>
</button>
)}
</For>
</Show>
</div>
</div>
</div>
</Show>

<button
type="button"
class="absolute top-3 left-4 flex items-center gap-1.5 text-[10px] font-bold text-gray-7 hover:text-gray-11 transition-colors uppercase tracking-widest z-10"
onClick={props.onModelClick}
disabled={props.busy}
>
<Zap size={10} class="text-gray-7 group-hover:text-amber-11 transition-colors" />
<span>{props.selectedModelLabel}</span>
</button>

<div class="p-3 pt-8 pb-3 px-4">
<Show when={props.showNotionBanner}>
<button
type="button"
class="w-full mb-2 flex items-center justify-between gap-3 rounded-xl border border-green-7/20 bg-green-7/10 px-3 py-2 text-left text-sm text-green-12 transition-colors hover:bg-green-7/15"
onClick={props.onNotionBannerClick}
>
<span>Try it now: set up my CRM in Notion</span>
<span class="text-xs text-green-12 font-medium">Insert prompt</span>
</button>
</Show>

<div class="relative">
<Show when={props.toast}>
<div class="absolute bottom-full right-0 mb-2 z-30 rounded-xl border border-gray-6 bg-gray-1/90 px-3 py-2 text-xs text-gray-11 shadow-lg backdrop-blur-md">
{props.toast}
</div>
</Show>

<div class="relative flex items-end gap-3">
<textarea
ref={textareaRef}
rows={1}
disabled={props.busy}
value={props.prompt}
onInput={(e) => props.setPrompt(e.currentTarget.value)}
onKeyDown={handleKeyDown}
placeholder="Ask OpenWork..."
class="flex-1 bg-transparent border-none p-0 text-gray-12 placeholder-gray-6 focus:ring-0 text-[15px] leading-relaxed resize-none min-h-[24px]"
/>

<button
disabled={!props.prompt.trim() || props.busy}
onClick={props.onSend}
class="p-2 bg-gray-12 text-gray-1 rounded-xl hover:scale-105 active:scale-95 transition-all disabled:opacity-0 disabled:scale-75 shadow-lg shrink-0 flex items-center justify-center"
title="Run"
>
<ArrowRight size={18} />
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
Loading