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
19 changes: 14 additions & 5 deletions apps/web/src/chat/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export function Composer({
onAttach,
onRunWorkflow,
onVoice,
voiceUnavailable,
sendLabel = "Send",
autoFocus,
}: {
readonly placeholder: string;
readonly busy: boolean;
Expand All @@ -35,6 +38,11 @@ export function Composer({
readonly onRunWorkflow?: () => void;
/** Starts voice mode; the mic is hidden without it. */
readonly onVoice?: () => void;
/** Shows the mic disabled with this title when there is no `onVoice`. */
readonly voiceUnavailable?: string;
/** Accessible name of the send button. */
readonly sendLabel?: string;
readonly autoFocus?: boolean;
}) {
const [text, setText] = useState("");
const [menuOpen, setMenuOpen] = useState(false);
Expand Down Expand Up @@ -219,6 +227,7 @@ export function Composer({
placeholder={placeholder}
aria-label={placeholder}
disabled={disabled}
autoFocus={autoFocus}
onChange={(event) => {
setText(event.target.value);
syncMention(event.target);
Expand Down Expand Up @@ -252,13 +261,13 @@ export function Composer({
}
}}
/>
{onVoice === undefined ? null : (
{onVoice === undefined && voiceUnavailable === undefined ? null : (
<button
type="button"
className="chat-composer-btn chat-composer-voice"
aria-label="Voice mode"
title="Voice mode"
disabled={disabled}
title={onVoice === undefined ? voiceUnavailable : "Voice mode"}
disabled={disabled || onVoice === undefined}
onClick={onVoice}
>
<AudioLines aria-hidden="true" />
Expand All @@ -269,8 +278,8 @@ export function Composer({
className="chat-composer-send"
disabled={!canSend}
onClick={send}
aria-label={busy ? "Sending…" : "Send"}
title={busy ? "Sending…" : "Send"}
aria-label={busy ? "Sending…" : sendLabel}
title={busy ? "Sending…" : sendLabel}
>
{busy ? (
<CircleNotch className="chat-composer-send-spinner" aria-hidden="true" />
Expand Down
87 changes: 25 additions & 62 deletions apps/web/src/pages/new-workbench-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
// A new workbench starts from one message: creating it deploys its agent and
// posts the message so the worker starts.

import { Button } from "@corbits/react-ui";
import { toast } from "@corbits/react-ui/ui/toast";
import { WorkbenchLoadingState } from "@/chat";
import { useState } from "react";
import { Composer } from "@/chat/composer";
import { useEffect, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { reportError } from "@corbits/error-sink";

import { Microphone, Paperclip } from "@/lib/icons";
import { useBench } from "../bench-context";
import { workbenchKeys } from "../chat-path";
import { NEW_WORKBENCH_TITLE, titleFromFirstMessage } from "@/auto-workbench-title";
Expand Down Expand Up @@ -36,7 +35,16 @@ export function NewWorkbenchPickerRoute() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { selectedTenantId } = useBench();
const [prompt, setPrompt] = useState("");

// Autofocus keeps the field quiet; the ring appears once Tab is used.
const [keyboard, setKeyboard] = useState(false);
useEffect(() => {
const onKey = (event: KeyboardEvent) => {
if (event.key === "Tab") setKeyboard(true);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);

const create = useMutation({
mutationFn: ({
Expand Down Expand Up @@ -74,76 +82,31 @@ export function NewWorkbenchPickerRoute() {
},
});

function submit() {
const trimmed = prompt.trim();
if (trimmed === "" || selectedTenantId === null || create.isPending) return;
create.mutate({ benchTenantId: selectedTenantId, openingMessage: trimmed });
function submit(text: string) {
if (selectedTenantId === null || create.isPending) return;
create.mutate({ benchTenantId: selectedTenantId, openingMessage: text });
}

return (
<div className="flex h-full min-h-0 flex-col">
<StageTopBar crumbs={[{ label: "New" }]} />
<div className="new-wrap">
<div className="new-wrap" data-keyboard={keyboard ? "" : undefined}>
{create.isPending ? (
// `delayMs={0}`: a genuine wait the instant the person sends.
<WorkbenchLoadingState delayMs={0} title="Setting up your workbench…" />
) : (
<>
<ProviderSkipBanner />
<h1 className="new-title">What should we work on?</h1>
<form
className="new-composer"
onSubmit={(event) => {
event.preventDefault();
submit();
}}
>
<textarea
className="new-composer-input"
aria-label="What should we work on?"
placeholder="Describe the job — your co-worker sets up the rest"
value={prompt}
rows={1}
autoFocus
onChange={(event) => setPrompt(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
submit();
}
}}
/>
<div className="new-composer-row">
{/* Attach: no stock attachment path exists on the new-workbench message. */}
<button
type="button"
className="new-icon-btn"
aria-label="Attach"
title="Attachments arrive once a workbench exists"
disabled
>
<Paperclip />
</button>
{/* Dictate: voice mode belongs to an open workbench, so it is unavailable here. */}
<button
type="button"
className="new-icon-btn"
aria-label="Dictate"
title="Voice starts inside a workbench"
disabled
>
<Microphone />
</button>
<span className="new-composer-spacer" />
<Button
type="submit"
aria-label="Start this workbench"
disabled={prompt.trim() === "" || selectedTenantId === null}
>
Start
</Button>
</div>
</form>
<Composer
placeholder="Describe the job — your co-worker sets up the rest"
busy={false}
disabled={selectedTenantId === null}
autoFocus
sendLabel="Start this workbench"
voiceUnavailable="Voice starts inside a workbench"
onSend={submit}
/>
<p className="new-hint">
No templates. Your co-worker asks what it needs, connects tools with you, and suggests
skills and workflows as you go.
Expand Down
60 changes: 2 additions & 58 deletions apps/web/src/pages/new-workbench.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,68 +13,12 @@
letter-spacing: -0.02em;
}

.new-composer {
padding: 16px 16px 10px 18px;
border-radius: 20px;
background: var(--surface);
box-shadow: var(--raised);
transition: box-shadow 150ms ease-out;
}

.new-composer:focus-within {
box-shadow:
0 0 0 1px var(--line-strong),
0 6px 20px -6px rgb(0 0 0 / 18%);
/* Quiet on autofocus; the ring marks keyboard focus once Tab is used. */
.new-wrap[data-keyboard] .chat-composer-input:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}

.new-composer-input {
display: block;
width: 100%;
min-height: 32px;
resize: none;
border: 0;
outline: none;
background: transparent;
font: inherit;
font-size: 15px;
line-height: 1.5;
padding: 5px 4px;
}

.new-composer-row {
display: flex;
align-items: center;
gap: 4px;
padding-top: 14px;
}

.new-composer-spacer {
flex: 1;
}

.new-icon-btn {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border: 0;
border-radius: 50%;
background: transparent;
color: var(--muted-foreground);
}

.new-icon-btn:disabled {
opacity: 0.5;
cursor: default;
}

.new-icon-btn svg {
width: 18px;
height: 18px;
}

.new-hint {
margin: 16px 0 0;
text-align: center;
Expand Down
Loading