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
21 changes: 21 additions & 0 deletions app/components/demo/ReasoningDisclosureDemo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { ReasoningDisclosure } from "~/registry/blocks/reasoning-disclosure";

const completedReasoning = `# Plan
Compare the user's request with the current chat state.
**Then** choose the smallest UI boundary that can be installed as source code.`;

const streamingReasoning = `Reading the latest message...
Checking available tools and context...
Drafting a concise response.`;
</script>

<template>
<div class="flex w-full max-w-xl flex-col gap-4">
<ReasoningDisclosure :text="completedReasoning" />
<ReasoningDisclosure
:text="streamingReasoning"
is-streaming
/>
</div>
</template>
80 changes: 80 additions & 0 deletions app/registry/blocks/reasoning-disclosure/ReasoningDisclosure.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { computed, ref, watch } from "vue";
import { ChevronDown } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";

export interface ReasoningDisclosureProps {
/** Reasoning text supplied by the consuming app */
text: string;
/** Whether the model is currently streaming reasoning content */
isStreaming?: boolean;
/** Label shown while streaming */
streamingLabel?: string;
/** Label shown after streaming is complete */
completeLabel?: string;
/** Initial open state when not streaming */
defaultOpen?: boolean;
/** Additional CSS classes */
class?: HTMLAttributes["class"];
}

const props = withDefaults(defineProps<ReasoningDisclosureProps>(), {
isStreaming: false,
streamingLabel: "Thinking...",
completeLabel: "Thoughts",
defaultOpen: false,
});

const open = ref(props.isStreaming || props.defaultOpen);

const lines = computed(() => cleanMarkdown(props.text).split("\n").filter(Boolean));

watch(() => props.isStreaming, (isStreaming) => {
if (isStreaming) {
open.value = true;
}
}, { immediate: true });

function cleanMarkdown(text: string): string {
return text
.replace(/\*\*(.+?)\*\*/g, "$1")
.replace(/\*(.+?)\*/g, "$1")
.replace(/`(.+?)`/g, "$1")
.replace(/^#+\s+/gm, "");
}
</script>

<template>
<Collapsible
v-model:open="open"
data-slot="reasoning-disclosure"
:class="cn('relative min-w-0 w-full text-pretty *:first:mt-0 *:last:mb-0', props.class)"
>
<CollapsibleTrigger as-child>
<Button
type="button"
class="group p-0! text-muted-foreground"
variant="link"
>
{{ isStreaming ? streamingLabel : completeLabel }}
<ChevronDown
v-if="text.length > 0"
class="size-4 transition-transform duration-200 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</Button>
</CollapsibleTrigger>

<CollapsibleContent>
<div
v-for="(line, index) in lines"
:key="index"
>
<span class="whitespace-pre-wrap text-sm font-normal text-muted-foreground">{{ line }}</span>
</div>
</CollapsibleContent>
</Collapsible>
</template>
1 change: 1 addition & 0 deletions app/registry/blocks/reasoning-disclosure/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ReasoningDisclosure, type ReasoningDisclosureProps } from "./ReasoningDisclosure.vue";
7 changes: 7 additions & 0 deletions content/docs/2.components/1.chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Unlike an integrated UI module, these registry items are installed as source cod
| [ChatMessage](/docs/components/chat-message) | Individual message bubble component with content slots, leading content, and action buttons. |
| [ChatPrompt](/docs/components/chat-prompt) | Prompt input wrapper for composing text areas, errors, and footer controls. |
| [ChatPromptSubmit](/docs/components/chat-prompt-submit) | Status-aware submit, stop, and retry button for prompt workflows. |
| [ReasoningDisclosure](/docs/components/reasoning-disclosure) | Collapsible disclosure for app-owned AI reasoning text. |

## Install

Expand All @@ -32,6 +33,12 @@ npx shadcn-vue@latest add "https://ui.stackhacker.io/r/chat-prompt.json"
npx shadcn-vue@latest add "https://ui.stackhacker.io/r/chat-prompt-submit.json"
```

Install reasoning disclosure when you want to show app-owned reasoning text.

```bash
npx shadcn-vue@latest add "https://ui.stackhacker.io/r/reasoning-disclosure.json"
```

## Basic Composition

```vue
Expand Down
61 changes: 61 additions & 0 deletions content/docs/2.components/8.reasoning-disclosure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: ReasoningDisclosure
description: Show app-owned AI reasoning text in a compact collapsible disclosure.
category: chat
---

::component-preview
---
name: ReasoningDisclosureDemo
---
::

## Installation

```bash
npx shadcn-vue@latest add "https://ui.stackhacker.io/r/reasoning-disclosure.json"
```

## Usage

```vue
<script setup lang="ts">
import { ReasoningDisclosure } from '@/components/reasoning-disclosure'

const reasoning = ref('Compare the user request with the current chat state.')
const isStreaming = ref(false)
</script>

<template>
<ReasoningDisclosure :text="reasoning" :is-streaming="isStreaming" />
</template>
```

## App-Owned Reasoning

`ReasoningDisclosure` renders reasoning text that your app already has. Your app owns model output, streaming state, message schemas, persistence, and whether reasoning should be shown.

The component performs light text cleanup for simple markdown markers. It is not a full markdown renderer.

## Examples

### Default

::component-preview
---
name: ReasoningDisclosureDemo
---
::

## API Reference

### Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `text` | `string` | — | Reasoning text supplied by the consuming app. |
| `isStreaming` | `boolean` | `false` | Opens the disclosure and shows the streaming label while reasoning is streaming. |
| `streamingLabel` | `string` | `'Thinking...'` | Label shown while streaming. |
| `completeLabel` | `string` | `'Thoughts'` | Label shown after streaming is complete. |
| `defaultOpen` | `boolean` | `false` | Initial open state when not streaming. |
| `class` | `string` | — | Additional CSS classes. |
4 changes: 4 additions & 0 deletions scripts/registry-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ const expectedItems: Record<string, ExpectedRegistryItem> = {
dependencies: ["@lucide/vue"],
registryDependencies: ["badge", "button", "card"],
},
"reasoning-disclosure": {
dependencies: ["@lucide/vue"],
registryDependencies: ["button", "collapsible"],
},
};

if (registryIndex) {
Expand Down
Loading