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 not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import { TokenDevtoolBadge } from "../TokenDevtoolBadge/TokenDevtoolBadge";
import {
CARD_ICON_BUTTON_CLASS,
CARD_SEND_BUTTON_CLASS,
COMPACT_ICON_BUTTON_CLASS,
COMPACT_SEND_BUTTON_CLASS,
getFilesFromClipboard,
} from "./helpers";
import { useChatInput } from "./useChatInput";
Expand Down Expand Up @@ -81,6 +83,9 @@ interface Props {
/** Card composer: the text always keeps its own row above the controls,
* instead of sharing a single pill row until it wraps. Empty state only. */
stacked?: boolean;
/** Compact composer for side panels: tighter radius, flat shadow, smaller
* controls, and no per-message connection chip. */
variant?: "default" | "compact";
}

export function ChatInput({
Expand All @@ -100,6 +105,7 @@ export function ChatInput({
hideSubmitWhenEmpty = false,
recipientPicker,
stacked = false,
variant = "default",
}: Props) {
const { isDryRun, setIsDryRun } = useCopilotUIStore();
// Still the CHAT_MODE_OPTION flag, which no longer names what it gates: the
Expand Down Expand Up @@ -265,6 +271,18 @@ export function ChatInput({
setAttachments((prev) => prev.filter((_, i) => i !== index));
}

const isCompact = variant === "compact";
const iconButtonClass = stacked
? CARD_ICON_BUTTON_CLASS
: isCompact
? COMPACT_ICON_BUTTON_CLASS
: undefined;
const sendButtonClass = stacked
? CARD_SEND_BUTTON_CLASS
: isCompact
? COMPACT_SEND_BUTTON_CLASS
: undefined;

return (
<form onSubmit={handleSubmit} className={cn("relative flex-1", className)}>
{mentions.isOpen && (
Expand All @@ -288,6 +306,8 @@ export function ChatInput({
// pill's deep shadow, so it reads as a surface the text sits on.
stacked &&
"gap-3 !rounded-3xl border-transparent px-3.5 pb-3.5 pt-3 shadow-[0_0_0_0.5px_rgba(0,0,0,0.08),0_1px_2px_rgba(0,0,0,0.05),0_2px_4px_rgba(0,0,0,0.02)] has-[[data-slot=input-group-control]:focus-visible]:border-transparent",
isCompact &&
"!rounded-xl border-zinc-200 shadow-[0_1px_2px_rgba(0,0,0,0.04)] has-[[data-slot=input-group-control]:focus-visible]:border-zinc-400",
isRecording &&
"border-red-400 ring-1 ring-red-400 has-[[data-slot=input-group-control]:focus-visible]:border-red-400 has-[[data-slot=input-group-control]:focus-visible]:ring-red-400",
)}
Expand All @@ -301,14 +321,15 @@ export function ChatInput({
<div
className={cn(
"flex w-full flex-wrap",
stacked ? "items-center" : "items-end",
stacked || isCompact ? "items-center" : "items-end",
)}
>
<InputGroupAddon
align="inline-start"
className={cn(
"order-none gap-1 py-1 pl-1.5",
stacked && "gap-1.5 p-0",
isCompact && "gap-0.5 py-1 pl-1",
)}
>
<ComposerPlusMenu
Expand All @@ -322,7 +343,7 @@ export function ChatInput({
CARD_ICON_BUTTON_CLASS,
"[&[aria-expanded=true]_svg]:rotate-45 [&_svg]:transition-transform [&_svg]:duration-200",
)
: undefined
: iconButtonClass
}
/>
{recipientPicker}
Expand All @@ -346,7 +367,10 @@ export function ChatInput({
disabled={isInputDisabled}
placeholder={resolvedPlaceholder}
onMultilineChange={setIsMultiline}
className={stacked ? "px-0.5 py-1" : undefined}
className={cn(
stacked && "px-0.5 py-1",
isCompact && "text-sm leading-5 md:text-sm",
)}
/>
{isRecording && !value && (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
Expand All @@ -362,12 +386,13 @@ export function ChatInput({
className={cn(
"order-none ml-auto gap-1 py-1 pr-1.5",
stacked && "gap-1.5 p-0",
isCompact && "gap-0.5 py-1 pr-1",
)}
>
{/* Connection and tier are per-message settings, so they remain
changeable between turns in an existing session. The card
composer leaves this to the page's top-right control. */}
{!stacked && (!hasSession || !isStreaming) && (
{!stacked && !isCompact && (!hasSession || !isStreaming) && (
<ConnectionPicker connectionLocked={hasSession} />
)}
{showAdvancedComposerControls && !hasSession && (
Expand All @@ -386,7 +411,7 @@ export function ChatInput({
isStreaming={isStreaming}
disabled={disabled || isTranscribing || isStreaming}
highlight={isMicGlowing}
className={stacked ? CARD_ICON_BUTTON_CLASS : undefined}
className={iconButtonClass}
onClick={() => {
dismissGlow();
toggleRecording();
Expand All @@ -412,22 +437,29 @@ export function ChatInput({
}
}
}}
className="size-[2.625rem] rounded-full border-zinc-800 bg-zinc-800 text-white hover:border-zinc-900 hover:bg-zinc-900 disabled:border-zinc-200 disabled:bg-zinc-200 disabled:text-white disabled:opacity-100"
className={cn(
"size-[2.625rem] rounded-full border-zinc-800 bg-zinc-800 text-white hover:border-zinc-900 hover:bg-zinc-900 disabled:border-zinc-200 disabled:bg-zinc-200 disabled:text-white disabled:opacity-100",
sendButtonClass,
)}
>
<Icon icon={ArrowUp02Icon} className="size-4" />
</PromptInputButton>
)}
{isStreaming ? (
<Tooltip>
<TooltipTrigger asChild>
<PromptInputSubmit status="streaming" onStop={onStop} />
<PromptInputSubmit
status="streaming"
onStop={onStop}
className={sendButtonClass}
/>
</TooltipTrigger>
<TooltipContent side="top">Stop</TooltipContent>
</Tooltip>
) : hideSubmitWhenEmpty && !canSend ? null : (
<PromptInputSubmit
disabled={!canSend}
className={stacked ? CARD_SEND_BUTTON_CLASS : undefined}
className={sendButtonClass}
/>
)}
</InputGroupAddon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,11 @@ export function formatElapsedTime(ms: number): string {
export const CARD_ICON_BUTTON_CLASS =
"size-9 rounded-full border-transparent bg-zinc-950/[0.06] p-0 text-zinc-900 shadow-none transition-[background-color,transform] hover:border-transparent hover:bg-zinc-950/10 hover:text-zinc-900 active:scale-[0.98] aria-expanded:bg-zinc-950/[0.12]";

export const COMPACT_ICON_BUTTON_CLASS =
"size-8 rounded-lg border-transparent bg-transparent p-0 text-zinc-700 shadow-none hover:border-transparent hover:bg-zinc-100 hover:text-zinc-900 aria-expanded:bg-zinc-100";

export const COMPACT_SEND_BUTTON_CLASS =
"size-8 rounded-lg border-zinc-900 bg-zinc-900 text-white hover:border-zinc-800 hover:bg-zinc-800 disabled:border-zinc-200 disabled:bg-zinc-200 disabled:text-white disabled:opacity-100";

export const CARD_SEND_BUTTON_CLASS =
"size-9 rounded-full border-transparent bg-zinc-950 text-white transition-[background-color,transform] hover:border-transparent hover:bg-zinc-800 active:scale-[0.98] disabled:border-transparent disabled:bg-zinc-950/[0.06] disabled:text-zinc-400";
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,12 @@ interface Props {
* every other host (share viewer, memory and builder panels) leaves this
* off, whatever the persisted panel state says. */
areFilesOpen?: boolean;
/** Compact thread for side panels: smaller text, tighter bubbles and
* spacing. */
variant?: "default" | "compact";
/** Hosts that already name the thread (e.g. the expert chat drawer)
* turn the floating identity chip off. */
showThreadHeader?: boolean;
}

/**
Expand Down Expand Up @@ -308,7 +314,10 @@ export function ChatMessagesContainer({
hasFloatingControls = false,
canOpenActivity = false,
areFilesOpen = false,
variant = "default",
showThreadHeader = true,
}: Props) {
const isCompact = variant === "compact";
const messages = useMemo(
() => revealKickoffMessages(allMessages),
[allMessages],
Expand Down Expand Up @@ -462,14 +471,16 @@ export function ChatMessagesContainer({

return (
<>
<ThreadHeader
expertIdentity={expertIdentity}
readOnly={readOnly}
sessionId={sessionID}
hasFloatingControls={hasFloatingControls}
canOpenActivity={canOpenActivity}
/>
<ChatMinimap messages={messages} />
{showThreadHeader && (
<ThreadHeader
expertIdentity={expertIdentity}
readOnly={readOnly}
sessionId={sessionID}
hasFloatingControls={hasFloatingControls}
canOpenActivity={canOpenActivity}
/>
)}
{!isCompact && <ChatMinimap messages={messages} />}
<Conversation
key={sessionID ?? "new"}
resize="instant"
Expand All @@ -483,6 +494,8 @@ export function ChatMessagesContainer({
<ConversationContent
className={cn(
"ease-[cubic-bezier(0.32,0.72,0,1)] mx-auto flex min-h-full w-full max-w-3xl flex-1 flex-col gap-6 px-6 pb-4 pt-14 transition-transform duration-300 will-change-transform motion-reduce:transition-none",
isCompact && "gap-4 px-4 pt-4",
!showThreadHeader && "pt-4",
areFilesOpen && "xl:-translate-x-40",
)}
style={
Expand Down Expand Up @@ -604,14 +617,16 @@ export function ChatMessagesContainer({
className="duration-300 animate-in fade-in slide-in-from-bottom-2 fill-mode-both"
>
<MessageContent
className={
"text-[1rem] leading-relaxed " +
"group-[.is-user]:rounded-3xl group-[.is-user]:bg-zinc-100 group-[.is-user]:px-4 group-[.is-user]:py-2.5 group-[.is-user]:text-zinc-900 " +
"group-[.is-user]:[&_h1]:text-lg group-[.is-user]:[&_h1]:font-semibold group-[.is-user]:[&_h2]:text-lg group-[.is-user]:[&_h2]:font-semibold group-[.is-user]:[&_h3]:text-lg group-[.is-user]:[&_h3]:font-semibold group-[.is-user]:[&_h4]:text-lg group-[.is-user]:[&_h4]:font-semibold group-[.is-user]:[&_h5]:text-lg group-[.is-user]:[&_h5]:font-semibold group-[.is-user]:[&_h6]:text-lg group-[.is-user]:[&_h6]:font-semibold " +
className={cn(
isCompact
? "text-sm leading-6 group-[.is-user]:rounded-xl"
: "text-[1rem] leading-relaxed group-[.is-user]:rounded-3xl",
"group-[.is-user]:bg-zinc-100 group-[.is-user]:px-4 group-[.is-user]:py-2.5 group-[.is-user]:text-zinc-900",
"group-[.is-user]:[&_h1]:text-lg group-[.is-user]:[&_h1]:font-semibold group-[.is-user]:[&_h2]:text-lg group-[.is-user]:[&_h2]:font-semibold group-[.is-user]:[&_h3]:text-lg group-[.is-user]:[&_h3]:font-semibold group-[.is-user]:[&_h4]:text-lg group-[.is-user]:[&_h4]:font-semibold group-[.is-user]:[&_h5]:text-lg group-[.is-user]:[&_h5]:font-semibold group-[.is-user]:[&_h6]:text-lg group-[.is-user]:[&_h6]:font-semibold",
// Chain hover pills use negative margins that the base
// overflow-hidden would clip.
"group-[.is-assistant]:overflow-visible group-[.is-assistant]:bg-transparent group-[.is-assistant]:text-slate-900"
}
"group-[.is-assistant]:overflow-visible group-[.is-assistant]:bg-transparent group-[.is-assistant]:text-slate-900",
)}
>
{isAssistant ? (
<ChainMessageParts
Expand Down Expand Up @@ -772,7 +787,14 @@ export function ChatMessagesContainer({
{!readOnly &&
queuedMessages?.map((msg, idx) => (
<Message key={idx} from="user">
<MessageContent className="flex flex-col gap-1 rounded-3xl border border-dashed border-zinc-300 bg-zinc-100 px-4 py-2.5 text-[1rem] leading-relaxed text-zinc-900 opacity-60">
<MessageContent
className={cn(
"flex flex-col gap-1 border border-dashed border-zinc-300 bg-zinc-100 px-4 py-2.5 text-zinc-900 opacity-60",
isCompact
? "rounded-xl text-sm leading-6"
: "rounded-3xl text-[1rem] leading-relaxed",
)}
>
<span>{msg}</span>
<span className="flex items-center gap-1 text-xs text-slate-500">
<Icon icon={Clock01Icon} className="size-3" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import { Icon } from "@/components/atoms/Icon/Icon";
import { cn } from "@/lib/utils";
import { Add01Icon, CheckmarkCircle02Icon } from "@hugeicons/core-free-icons";
import Image from "next/image";
import { useState } from "react";
Expand All @@ -13,6 +14,7 @@ interface Props {
isConnected?: boolean;
/** Second line, used by the recommended list to show the model's reason. */
description?: string | null;
className?: string;
}

// The welcome dialog's provider card: compact (two per row), name plus an
Expand All @@ -23,6 +25,7 @@ export function ConnectProviderRow({
onSelect,
isConnected,
description,
className,
}: Props) {
const src = `/integrations/${provider.id}.png`;
const [brokenSrc, setBrokenSrc] = useState<string | null>(null);
Expand All @@ -32,7 +35,10 @@ export function ConnectProviderRow({
<button
type="button"
onClick={() => onSelect(provider.id)}
className="group flex h-14 w-full items-center gap-2.5 rounded-xl bg-neutral-100 px-3 text-left transition-colors hover:bg-neutral-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 active:bg-neutral-200"
className={cn(
"group flex h-14 w-full items-center gap-2.5 rounded-xl bg-neutral-100 px-3 text-left transition-colors hover:bg-neutral-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-purple-400 active:bg-neutral-200",
className,
)}
>
{broken ? (
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AiEraserIcon, ArrowRight01Icon } from "@hugeicons/core-free-icons";
import Link from "next/link";
import type { HomeDashboardResponse } from "@/app/api/__generated__/models/homeDashboardResponse";
import { Icon } from "@/components/atoms/Icon/Icon";
import { Text } from "@/components/atoms/Text/Text";
import { formatWeeklySpend } from "../../helpers";
import { HomeTileEmpty } from "../HomeTileEmpty/HomeTileEmpty";
import { HomeTile } from "../HomeTile/HomeTile";
Expand Down Expand Up @@ -50,9 +51,16 @@ export function AgentTeam({ dashboard, className }: Props) {
{team.total > 0 ? (
<Link
href="/team"
className="flex items-center justify-between border-t border-zinc-100 px-4 py-2 text-xs font-medium text-zinc-500 outline-none transition-colors hover:bg-zinc-50 hover:text-zinc-900 focus-visible:bg-zinc-50"
className="group flex items-center justify-between border-t border-zinc-100 px-4 py-2 outline-none transition-colors hover:bg-zinc-50 focus-visible:bg-zinc-50"
>
View all {team.total} experts
<Text
variant="small-medium"
as="span"
tone="muted"
className="transition-colors group-hover:text-zinc-900"
>
View all {team.total} experts
</Text>
<Icon icon={ArrowRight01Icon} size={14} aria-hidden="true" />
</Link>
) : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ interface Props {
agent: HomeAgentStatus;
}

const ACTION_CLASS = "h-7 min-w-0 rounded-md px-2.5 text-xs";

/** One expert with its status and two ways in. The detail line is gone:
* anything that needs doing is already spelled out under Needs you. */
export function AgentRow({ agent }: Props) {
Expand All @@ -32,7 +30,8 @@ export function AgentRow({ agent }: Props) {
<div className="flex items-center gap-2">
<Text
variant="body-medium"
className="truncate leading-5 text-zinc-900"
tone="primary"
className="truncate leading-5"
>
{agent.expert.name}
</Text>
Expand All @@ -41,7 +40,8 @@ export function AgentRow({ agent }: Props) {
{secondLine ? (
<Text
variant="small"
className="truncate tabular-nums leading-4 text-zinc-500"
tone="muted"
className="truncate tabular-nums leading-4"
unmask={false}
>
{secondLine}
Expand All @@ -53,8 +53,7 @@ export function AgentRow({ agent }: Props) {
as="NextLink"
href={`/copilot?expertId=${agent.expert.id}`}
variant="secondary"
size="small"
className={ACTION_CLASS}
size="xs"
aria-label={`Chat with ${agent.expert.name}`}
>
Chat
Expand All @@ -63,8 +62,7 @@ export function AgentRow({ agent }: Props) {
as="NextLink"
href={`/team/${agent.expert.id}`}
variant="secondary"
size="small"
className={ACTION_CLASS}
size="xs"
aria-label={`Manage ${agent.expert.name}`}
>
Manage
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { HomeAgentStatus } from "@/app/api/__generated__/models/homeAgentStatus";
import { Text } from "@/components/atoms/Text/Text";
import { cn } from "@/lib/utils";

const STATUS_CONFIG = {
Expand All @@ -25,7 +26,12 @@ export function StatusBadge({ status }: Props) {
const config = STATUS_CONFIG[status] ?? UNKNOWN_STATUS_CONFIG;

return (
<span className="inline-flex shrink-0 items-center gap-1.5 text-[11px] font-medium text-zinc-500">
<Text
variant="small-medium"
as="span"
tone="muted"
className="inline-flex shrink-0 items-center gap-1.5"
>
<span
className={cn(
"size-1.5 rounded-full",
Expand All @@ -35,6 +41,6 @@ export function StatusBadge({ status }: Props) {
aria-hidden="true"
/>
{config.label}
</span>
</Text>
);
}
Loading
Loading