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
1 change: 1 addition & 0 deletions apps/docs/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@import "fumadocs-ui/css/preset.css";
@import "../node_modules/tw-shimmer/src/index.css";
@source "../node_modules/@assistant-ui/chords/dist";
@source inline("shimmer");

/* Safelist color utilities for playground dynamic class inputs.
Tailwind 4 @source inline() scans for literal class names — no brace expansion. */
Expand Down
6 changes: 6 additions & 0 deletions apps/docs/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ const majorChords = [
description: "File and image attachments for composer and messages.",
href: "/docs/major-chords/attachment",
},
{
name: "ReasoningAccordion",
description:
"Collapsible accordion for AI reasoning with auto-expand during streaming.",
href: "/docs/major-chords/reasoning-accordion",
},
];

const minorChords = [
Expand Down
108 changes: 108 additions & 0 deletions apps/docs/components/demo/reasoning-accordion-demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"use client";

import {
AssistantRuntimeProvider,
MessagePrimitive,
ThreadPrimitive,
useLocalRuntime,
} from "@assistant-ui/react";
import { ReasoningAccordion } from "@assistant-ui/chords";
import { DemoWrapper } from "./demo-wrapper";
import type { ChatModelAdapter } from "@assistant-ui/react";
import { FC } from "react";

const REASONING =
"Let me think about this step by step. First, I need to consider the quantum mechanical principles involved. Entanglement occurs when particles become correlated in such a way that the quantum state of each particle cannot be described independently of the others.";

const TEXT =
"Quantum entanglement is a phenomenon where two particles become linked, so measuring one instantly affects the other — no matter the distance between them. Einstein famously called it 'spooky action at a distance.'";

const demoAdapter: ChatModelAdapter = {
async *run({ abortSignal }) {
for (let i = 0; i < REASONING.length; i++) {
if (abortSignal.aborted) return;
await new Promise((r) => setTimeout(r, 8));
yield {
content: [
{ type: "reasoning" as const, text: REASONING.slice(0, i + 1) },
],
};
}

for (let i = 0; i < TEXT.length; i++) {
if (abortSignal.aborted) return;
await new Promise((r) => setTimeout(r, 12));
yield {
content: [
{ type: "reasoning" as const, text: REASONING },
{ type: "text" as const, text: TEXT.slice(0, i + 1) },
],
};
}
},
};

export function ReasoningAccordionDemo() {
const runtime = useLocalRuntime(demoAdapter);

return (
<DemoWrapper>
<AssistantRuntimeProvider runtime={runtime}>
<ThreadPrimitive.Root className="flex h-87.5 flex-col rounded-xl border border-zinc-400 dark:border-zinc-800 bg-white text-zinc-900 dark:bg-zinc-950 dark:text-white px-8">
<div className="relative min-h-0 flex-1">
<ThreadPrimitive.Viewport className="flex h-full flex-col gap-2 overflow-y-auto px-4 pt-4 pb-4">
<ThreadPrimitive.Messages
components={{
UserMessage: DemoUserMessage,
AssistantMessage: DemoAssistantMessage,
}}
/>
</ThreadPrimitive.Viewport>
</div>
<div className="border-t border-zinc-300 dark:border-zinc-800 px-4 py-2">
<ThreadPrimitive.If running={false}>
<ThreadPrimitive.Suggestion
prompt="Explain quantum entanglement"
send
className="w-full rounded-lg dark:bg-white/5 bg-black/10 px-3 py-2 text-left text-sm dark:text-white/60 text-black dark:hover:bg-white/10 hover:bg-black/15 transition-colors"
>
Send a message to trigger the reasoning demo
</ThreadPrimitive.Suggestion>
</ThreadPrimitive.If>
</div>
</ThreadPrimitive.Root>
</AssistantRuntimeProvider>
</DemoWrapper>
);
}

const DemoUserMessage: FC = () => (
<MessagePrimitive.Root className="group/message mx-auto flex w-full max-w-3xl flex-col items-end gap-1">
<div className="max-w-[80%] rounded-3xl bg-zinc-100 px-5 text-zinc-900 dark:bg-white/10 dark:text-white/90">
<MessagePrimitive.Content />
</div>
</MessagePrimitive.Root>
);

function DemoAssistantMessage() {
return (
<MessagePrimitive.Root className="group/message mx-auto flex w-full max-w-3xl gap-3">
<div className="mt-2.5 flex size-8 shrink-0 items-center justify-center rounded-full border border-zinc-300 text-xs shadow dark:border-white/15">
A
</div>
<div className="flex-1 pt-0.5">
<MessagePrimitive.Parts
components={{
Text: ({ text }) => (
<span className="text-sm dark:text-white/90 text-black">
{text}
</span>
),
Reasoning: ({ text }) => <span>{text}</span>,
ReasoningGroup: ReasoningAccordion,
}}
/>
</div>
</MessagePrimitive.Root>
);
}
14 changes: 14 additions & 0 deletions apps/docs/components/playground/code-generators/generate-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,20 @@ const generators: Record<ChordId, (config: ChordConfig) => CodeGenResult> = {
</ThreadPrimitive.Empty>`,
};
},
"reasoning-accordion": (config) => {
const defaults = chordRegistry["reasoning-accordion"].defaultConfig;
const props = buildPropsString(config, defaults);
return {
imports: `import { ReasoningAccordion } from "@assistant-ui/chords";`,
jsx: `<MessagePrimitive.Parts
components={{
Text: ({ text }) => <span>{text}</span>,
Reasoning: ({ text }) => <span>{text}</span>,
ReasoningGroup: ReasoningAccordion,
}}
/>`,
};
},
"scroll-to-bottom": (config) => {
const defaults = chordRegistry["scroll-to-bottom"].defaultConfig;
const props = buildPropsString(config, defaults);
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/components/playground/controls-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SuggestionChipsControls } from "./controls/suggestion-chips-controls";
import { ThreadEmptyControls } from "./controls/thread-empty-controls";
import { AttachmentControls } from "./controls/attachment-controls";
import { ScrollToBottomControls } from "./controls/scroll-to-bottom-controls";
import { ReasoningAccordionControls } from "./controls/reasoning-accordion-controls";

type ControlsPanelProps = {
chordId: ChordId;
Expand All @@ -33,6 +34,7 @@ const controlsMap: Partial<
"thread-empty": ThreadEmptyControls,
attachment: AttachmentControls,
"scroll-to-bottom": ScrollToBottomControls,
"reasoning-accordion": ReasoningAccordionControls,
};

export function ControlsPanel({ chordId, config, onChange }: ControlsPanelProps) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import type { ChordConfig } from "@/lib/playground/types";
import { TextInput, StyleBuilder } from "./shared";

export function ReasoningAccordionControls({
config,
onChange,
}: {
config: ChordConfig;
onChange: (c: ChordConfig) => void;
}) {
return (
<div className="flex flex-col gap-3">
<TextInput
label="label"
value={(config.label as string) ?? "Reasoning"}
onChange={(v) => onChange({ ...config, label: v || undefined })}
placeholder="Reasoning"
/>
<StyleBuilder
label="className"
value={(config.className as string) ?? ""}
defaultValue=""
onChange={(v) => onChange({ ...config, className: v || undefined })}
controls={["bg", "text", "rounded", "p"]}
/>
<StyleBuilder
label="triggerClassName"
value={(config.triggerClassName as string) ?? ""}
defaultValue=""
onChange={(v) =>
onChange({ ...config, triggerClassName: v || undefined })
}
controls={["bg", "text"]}
/>
<StyleBuilder
label="contentClassName"
value={(config.contentClassName as string) ?? ""}
defaultValue=""
onChange={(v) =>
onChange({ ...config, contentClassName: v || undefined })
}
controls={["bg", "text"]}
/>
</div>
);
}
2 changes: 2 additions & 0 deletions apps/docs/components/playground/preview-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { CopyButtonPreview } from "./previews/copy-button-preview";
import { SuggestionChipsPreview } from "./previews/suggestion-chips-preview";
import { ThreadEmptyPreview } from "./previews/thread-empty-preview";
import { ScrollToBottomPreview } from "./previews/scroll-to-bottom-preview";
import { ReasoningAccordionPreview } from "./previews/reasoning-accordion-preview";

type PreviewPanelProps = {
chordId: ChordId;
Expand All @@ -36,6 +37,7 @@ const previewMap: Record<ChordId, React.FC<{ config: ChordConfig }>> = {
"suggestion-chips": SuggestionChipsPreview,
"thread-empty": ThreadEmptyPreview,
"scroll-to-bottom": ScrollToBottomPreview,
"reasoning-accordion": ReasoningAccordionPreview,
};

export function PreviewPanel({ chordId, config }: PreviewPanelProps) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use client";

import {
AssistantRuntimeProvider,
ThreadPrimitive,
MessagePrimitive,
useLocalRuntime,
} from "@assistant-ui/react";
import { ReasoningAccordion } from "@assistant-ui/chords";
import type { ChordConfig } from "@/lib/playground/types";
import { createReasoningAdapter } from "@/lib/playground/mock-runtime";
import { useRef } from "react";

const UserMessage = () => (
<MessagePrimitive.Root className="mx-auto flex w-full max-w-3xl flex-col items-end gap-1">
<div className="max-w-[80%] rounded-3xl bg-zinc-100 px-5 text-zinc-900 dark:bg-white/10 dark:text-white/90">
<MessagePrimitive.Content />
</div>
</MessagePrimitive.Root>
);

const AssistantMessage = ({ config }: { config: ChordConfig }) => (
<MessagePrimitive.Root className="group/message mx-auto flex w-full max-w-3xl gap-3">
<div className="mt-2.5 flex size-8 shrink-0 items-center justify-center rounded-full border border-zinc-300 text-xs shadow dark:border-white/15">
A
</div>
<div className="flex-1 pt-0.5">
<MessagePrimitive.Parts
components={{
Text: ({ text }) => (
<span className="text-sm text-zinc-900 dark:text-white/90">
{text}
</span>
),
Reasoning: ({ text }) => <span>{text}</span>,
ReasoningGroup: ((props: any) => (
<ReasoningAccordion
{...props}
label={(config.label as string) || undefined}
className={(config.className as string) || undefined}
triggerClassName={
(config.triggerClassName as string) || undefined
}
contentClassName={
(config.contentClassName as string) || undefined
}
textClassName={(config.textClassName as string) || undefined}
/>
)) as any,
}}
/>
</div>
</MessagePrimitive.Root>
);

export function ReasoningAccordionPreview({
config,
}: {
config: ChordConfig;
}) {
const adapter = useRef(createReasoningAdapter());
const runtime = useLocalRuntime(adapter.current);

return (
<AssistantRuntimeProvider runtime={runtime}>
<ThreadPrimitive.Root className="flex h-80 flex-col rounded-xl border border-zinc-300 dark:border-zinc-800 bg-white dark:bg-zinc-950 text-zinc-900 dark:text-white">
<div className="relative min-h-0 flex-1">
<ThreadPrimitive.Viewport className="flex h-full flex-col gap-2 overflow-y-auto px-4 pt-4 pb-4">
<ThreadPrimitive.Messages
components={{
UserMessage,
AssistantMessage: () => <AssistantMessage config={config} />,
}}
/>
</ThreadPrimitive.Viewport>
</div>
<div className="border-t border-zinc-200 dark:border-zinc-800 px-4 py-2">
<ThreadPrimitive.If running={false}>
<ThreadPrimitive.Suggestion
prompt="Explain quantum entanglement"
send
className="w-full rounded-lg bg-zinc-100 dark:bg-white/5 px-3 py-2 text-left text-sm text-zinc-600 dark:text-white/60 transition-colors hover:bg-zinc-200 dark:hover:bg-white/10"
>
Click to trigger reasoning demo
</ThreadPrimitive.Suggestion>
</ThreadPrimitive.If>
</div>
</ThreadPrimitive.Root>
</AssistantRuntimeProvider>
);
}
3 changes: 2 additions & 1 deletion apps/docs/content/docs/major-chords/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"edit-composer",
"follow-up-suggestions",
"attachment",
"tool-call-renderer"
"tool-call-renderer",
"reasoning-accordion"
]
}
Loading