Skip to content
Open
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ primitives for React — and the documentation site and playground at

The package ships behavior, state, and wire formats with no styling of its own: a contenteditable
composer with commands and chips, a thread with scroll auto-follow, message part segmentation, tool-call
timelines. This is the Base UI model applied to chat — the package owns behavior, you own every class.
timelines, and the app-shell parts around them — a collapsible sidebar, a nav tree, a closable tab
collection. This is the Base UI model applied to chat — the package owns behavior, you own every class.

## Layout

Expand Down
7 changes: 6 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 12 additions & 10 deletions components/ai/ask-user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import {
useAskUserOption,
useAskUserOptions,
} from "@intentface/chat/ask-user";
import { CheckIcon } from "lucide-react";
import {
IconCheck,
IconChevronDown,
IconChevronLeft,
IconChevronRight,
IconHelpCircle,
} from "@tabler/icons-react";
import { type ComponentProps, type RefObject, useState } from "react";
import { ChevronDownIcon } from "@/components/icons/chevron-down";
import { ChevronLeftMediumIcon } from "@/components/icons/chevron-left-medium";
import { ChevronRightMediumIcon } from "@/components/icons/chevron-right-medium";
import { CircleQuestionmarkIcon } from "@/components/icons/circle-questionmark";
import { Collapsible } from "@/components/ui/collapsible";
import type { AskUserQuestion } from "@/lib/ai/types";
import { cn } from "@/lib/utils";
Expand Down Expand Up @@ -60,7 +62,7 @@ type AskUserPreviousProps = ComponentProps<typeof AskUserPrimitive.Previous>;

const AskUserPrevious = ({ className, ...props }: AskUserPreviousProps) => (
<AskUserPrimitive.Previous className={cn(navigationButtonClasses, className)} {...props}>
<ChevronLeftMediumIcon className="size-3.5" />
<IconChevronLeft className="size-3.5" />
</AskUserPrimitive.Previous>
);

Expand All @@ -69,7 +71,7 @@ type AskUserNextProps = ComponentProps<typeof AskUserPrimitive.Next>;

const AskUserNext = ({ className, ...props }: AskUserNextProps) => (
<AskUserPrimitive.Next className={cn(navigationButtonClasses, className)} {...props}>
<ChevronRightMediumIcon className="size-3.5" />
<IconChevronRight className="size-3.5" />
</AskUserPrimitive.Next>
);

Expand Down Expand Up @@ -138,7 +140,7 @@ const AskUserOptionCheckIndicator = () => {
aria-hidden="true"
className={cn(INDICATOR_CLASS, option.selected && INDICATOR_SELECTED_CLASS)}
>
{option.selected && <CheckIcon className="size-3" />}
{option.selected && <IconCheck className="size-3" />}
</span>
);
};
Expand Down Expand Up @@ -206,7 +208,7 @@ const AskUserSummary = ({ questions, answers, className, ...props }: AskUserSumm
<span className="text-gray-11/60">
Answered {count} {count === 1 ? "question" : "questions"}
</span>
<ChevronDownIcon
<IconChevronDown
className={cn(
"size-3.5 shrink-0 transition-transform",
isOpen ? "rotate-180" : "rotate-0",
Expand All @@ -217,7 +219,7 @@ const AskUserSummary = ({ questions, answers, className, ...props }: AskUserSumm
{questions.map((q) => (
<div key={q.question} className="flex gap-2 leading-tight">
<div className="flex h-lh shrink-0 items-center justify-center">
<CircleQuestionmarkIcon className="size-4" />
<IconHelpCircle className="size-4" />
</div>
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium leading-tight">{q.question}</span>
Expand Down
13 changes: 5 additions & 8 deletions components/ai/attachments.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import type { AttachmentErrorCode, AttachmentItem } from "@intentface/chat/attachments";
import { PaperclipIcon } from "lucide-react";
import { IconFile, IconPaperclip, IconX } from "@tabler/icons-react";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import type { ComponentProps, ReactNode } from "react";
Expand All @@ -10,9 +10,6 @@ import { createPortal } from "react-dom";
import { IconButton } from "@/components/ui/icon-button";
import { formatFileSize, isImageAttachment, isPdfAttachment } from "@/lib/ai/attachments";
import { cn } from "@/lib/utils";
import { CrossMediumIcon } from "../icons/cross-medium";
import { FileBendIcon } from "../icons/file-bend";
import { PaperClipIcon } from "../icons/paperclip";

// Re-export the attachment surface so consumers import everything from this
// module: the generic mechanics from the headless package, and this app's
Expand Down Expand Up @@ -67,8 +64,8 @@ type AttachmentsItemProps = {
};

const getFileIcon = (mediaType: string) => {
if (isPdfAttachment(mediaType)) return FileBendIcon;
return PaperclipIcon;
if (isPdfAttachment(mediaType)) return IconFile;
return IconPaperclip;
};

const AttachmentsItem = ({ item, children, className }: AttachmentsItemProps) => {
Expand Down Expand Up @@ -138,7 +135,7 @@ const AttachmentsRemove = ({ onRemove, filename, className }: AttachmentsRemoveP
size="2xs"
type="button"
>
<CrossMediumIcon className="size-3" />
<IconX className="size-3" />
</IconButton>
);

Expand Down Expand Up @@ -224,7 +221,7 @@ type AttachmentsTriggerProps = ComponentProps<typeof IconButton>;
const AttachmentsTrigger = ({ children, className, ...props }: AttachmentsTriggerProps) => {
return (
<IconButton type="button" variant="ghost" aria-label="Add attachment" {...props}>
{children ?? <PaperClipIcon />}
{children ?? <IconPaperclip />}
</IconButton>
);
};
Expand Down
5 changes: 2 additions & 3 deletions components/ai/composer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import { IconArrowUp, IconPlayerStop } from "@tabler/icons-react";

// Styled composer — thin wrappers over @intentface/chat/composer. Behavior
// (editor, store, machines, command plumbing) lives in the package; this file
Expand All @@ -20,8 +21,6 @@ import { Children, type ComponentProps, type ReactNode, useMemo, useRef } from "
import { AskUser } from "@/components/ai/ask-user";
import { Attachments } from "@/components/ai/attachments";
import { Chip } from "@/components/ai/chip";
import { SendIcon } from "@/components/icons/send";
import { StopIcon } from "@/components/icons/stop";
import Button from "@/components/ui/button";
import { IconButton } from "@/components/ui/icon-button";
import { Kbd } from "@/components/ui/kbd";
Expand Down Expand Up @@ -336,7 +335,7 @@ const ComposerSubmit = ({
transition={{ duration: 0.15 }}
className="flex items-center justify-center"
>
{isGenerating ? <StopIcon /> : (children ?? <SendIcon />)}
{isGenerating ? <IconPlayerStop /> : (children ?? <IconArrowUp />)}
</motion.span>
</AnimatePresence>
</IconButton>
Expand Down
22 changes: 12 additions & 10 deletions components/ai/message.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ import {
useMessageSelection,
useMessageSelectionScope,
} from "@intentface/chat/message";
import { PaperclipIcon } from "lucide-react";
import {
IconCheck,
IconCopy,
IconFile,
IconMessage,
IconPaperclip,
IconPlayerStop,
} from "@tabler/icons-react";
import { motion } from "motion/react";
import Image from "next/image";
import { type ComponentProps, useMemo } from "react";
Expand All @@ -21,11 +28,6 @@ import { useCopy } from "@/hooks/use-copy";
import { isImageAttachment, isPdfAttachment } from "@/lib/ai/attachments";
import { CHIP_ICONS, isChipIconKey } from "@/lib/ai/chip-icons";
import { cn } from "@/lib/utils";
import { Bubble5Icon } from "../icons/bubble-5";
import { CheckMarkMediumIcon } from "../icons/check-mark-medium";
import { CopyIcon } from "../icons/copy";
import { FileBendIcon } from "../icons/file-bend";
import { StopIcon } from "../icons/stop";

type MessageRootProps = ComponentProps<typeof MessagePrimitive.Root>;

Expand Down Expand Up @@ -220,7 +222,7 @@ const MessageStopped = ({ className, ...props }: ComponentProps<"div">) => (
{...props}
>
<span className="inline-flex items-center gap-1.5 rounded-full border border-primary-border bg-primary-bg px-2.5 py-1 text-xs text-ink-secondary">
<StopIcon className="size-3 shrink-0" />
<IconPlayerStop className="size-3 shrink-0" />
Stopped
</span>
</div>
Expand Down Expand Up @@ -256,7 +258,7 @@ const MessageCopy = ({
className={className}
{...props}
>
{isCopied ? <CheckMarkMediumIcon /> : <CopyIcon />}
{isCopied ? <IconCheck /> : <IconCopy />}
</IconButton>
}
/>
Expand All @@ -283,7 +285,7 @@ type MessageAttachmentProps = {
const MessageAttachment = ({ attachment, className, ...props }: MessageAttachmentProps) => {
const mediaType = attachment.mediaType ?? "";
const filename = attachment.filename ?? "Attachment";
const Icon = isPdfAttachment(mediaType) ? FileBendIcon : PaperclipIcon;
const Icon = isPdfAttachment(mediaType) ? IconFile : IconPaperclip;

return (
<div
Expand Down Expand Up @@ -407,7 +409,7 @@ const MessageSelection = ({ onAdd, className }: MessageSelectionProps) => {
window.getSelection()?.removeAllRanges();
}}
>
<Bubble5Icon className="size-3.5" />
<IconMessage className="size-3.5" />
Add to chat
</Button>
</PopoverPrimitive.Popup>
Expand Down
7 changes: 3 additions & 4 deletions components/ai/reasoning.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
"use client";

import { Reasoning as ReasoningPrimitive, useReasoning } from "@intentface/chat/reasoning";
import { IconBrain, IconChevronDown } from "@tabler/icons-react";
import { AnimatePresence, motion } from "motion/react";
import { type ComponentProps, memo, type ReactNode } from "react";
import { ChevronDownIcon } from "@/components/icons/chevron-down";
import { Markdown } from "@/components/ui/markdown";
import { splitReasoningByHeaders } from "@/lib/ai/message-info";
import { cn } from "@/lib/utils";
import { BrainIcon } from "../icons/brain";
import { TextShimmer } from "../ui/text-shimmer";

export { useReasoning };
Expand Down Expand Up @@ -76,7 +75,7 @@ const ReasoningTrigger = memo(
{children ?? (
<>
<div className="flex items-center gap-1">
<BrainIcon className="size-4" />
<IconBrain className="size-4" />
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={key}
Expand All @@ -90,7 +89,7 @@ const ReasoningTrigger = memo(
</motion.span>
</AnimatePresence>
</div>
<ChevronDownIcon
<IconChevronDown
className={cn("size-5 transition-transform", isOpen ? "rotate-180" : "rotate-0")}
/>
</>
Expand Down
4 changes: 2 additions & 2 deletions components/ai/thread.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
useThread,
useThreadVisibility,
} from "@intentface/chat/thread";
import { IconArrowDown } from "@tabler/icons-react";
import { AnimatePresence, motion } from "motion/react";
import type { ComponentProps } from "react";
import { memo, useCallback } from "react";
import Button from "@/components/ui/button";
import { ProgressiveBlur } from "@/components/ui/progressive-blur";
import { cn } from "@/lib/utils";
import { ArrowDownIcon } from "../icons/arrow-down";

export { useThread, useThreadVisibility };
export type { ThreadAutoScrollMode, ThreadVisibilityState };
Expand Down Expand Up @@ -148,7 +148,7 @@ const ThreadScrollButton = ({ className, ...props }: ThreadScrollButtonProps) =>
onClick={handleScrollToBottom}
className="rounded-full shadow-xs"
>
<ArrowDownIcon />
<IconArrowDown />
Latest
</Button>
</motion.div>
Expand Down
27 changes: 14 additions & 13 deletions components/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
"use client";

import {
IconBook,
IconBrandGithub,
IconBrandNpm,
IconDotsVertical,
IconPlus,
IconTrash,
} from "@tabler/icons-react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { IntentfaceLogo } from "@/components/icons/intentface-logo";
import { PlusMediumIcon } from "@/components/icons/plus-medium";
import { TrashIcon } from "@/components/icons/trash";
import { Sidebar } from "@/components/ui/sidebar";
import { deleteChatInstance } from "@/lib/chat-instance";
import { useChatStore } from "@/lib/store/chat";
import { BookIcon } from "./icons/book";
import { DotGrid1x3VerticalTightIcon } from "./icons/dot-grid-1x3-vertical-tight";
import { GitHubIcon } from "./icons/github";
import { NpmIcon } from "./icons/npm";
import DropdownMenu from "./ui/dropdown-menu";

export const AppSidebar = () => {
Expand Down Expand Up @@ -43,7 +44,7 @@ export const AppSidebar = () => {
isActive={pathname === "/playground"}
render={
<Link href="/playground">
<PlusMediumIcon />
<IconPlus />
<span>New Chat</span>
</Link>
}
Expand All @@ -66,7 +67,7 @@ export const AppSidebar = () => {
<DropdownMenu.Trigger
render={
<Sidebar.MenuAction showOnHover>
<DotGrid1x3VerticalTightIcon />
<IconDotsVertical />
<span className="sr-only">Delete</span>
</Sidebar.MenuAction>
}
Expand All @@ -76,7 +77,7 @@ export const AppSidebar = () => {
onClick={() => handleDelete(chat.id)}
aria-label="Delete"
>
<TrashIcon />
<IconTrash />
Delete
<span className="sr-only">Delete</span>
</DropdownMenu.Item>
Expand All @@ -97,7 +98,7 @@ export const AppSidebar = () => {
<Sidebar.MenuButton
render={
<Link href="/">
<BookIcon />
<IconBook />
<span>Docs</span>
</Link>
}
Expand All @@ -111,7 +112,7 @@ export const AppSidebar = () => {
target="_blank"
rel="noreferrer"
>
<GitHubIcon />
<IconBrandGithub />
<span>GitHub</span>
</a>
}
Expand All @@ -125,7 +126,7 @@ export const AppSidebar = () => {
target="_blank"
rel="noreferrer"
>
<NpmIcon />
<IconBrandNpm />
<span>npm</span>
</a>
}
Expand Down
Loading
Loading