Skip to content

Commit e1d8f52

Browse files
committed
feat(registry): Add reasoning disclosure component
1 parent bacd0ce commit e1d8f52

6 files changed

Lines changed: 174 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script setup lang="ts">
2+
import { ReasoningDisclosure } from "~/registry/blocks/reasoning-disclosure";
3+
4+
const completedReasoning = `# Plan
5+
Compare the user's request with the current chat state.
6+
**Then** choose the smallest UI boundary that can be installed as source code.`;
7+
8+
const streamingReasoning = `Reading the latest message...
9+
Checking available tools and context...
10+
Drafting a concise response.`;
11+
</script>
12+
13+
<template>
14+
<div class="flex w-full max-w-xl flex-col gap-4">
15+
<ReasoningDisclosure :text="completedReasoning" />
16+
<ReasoningDisclosure
17+
:text="streamingReasoning"
18+
is-streaming
19+
/>
20+
</div>
21+
</template>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<script setup lang="ts">
2+
import type { HTMLAttributes } from "vue";
3+
import { computed, ref, watch } from "vue";
4+
import { ChevronDown } from "@lucide/vue";
5+
import { Button } from "@/components/ui/button";
6+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
7+
import { cn } from "@/lib/utils";
8+
9+
export interface ReasoningDisclosureProps {
10+
/** Reasoning text supplied by the consuming app */
11+
text: string;
12+
/** Whether the model is currently streaming reasoning content */
13+
isStreaming?: boolean;
14+
/** Label shown while streaming */
15+
streamingLabel?: string;
16+
/** Label shown after streaming is complete */
17+
completeLabel?: string;
18+
/** Initial open state when not streaming */
19+
defaultOpen?: boolean;
20+
/** Additional CSS classes */
21+
class?: HTMLAttributes["class"];
22+
}
23+
24+
const props = withDefaults(defineProps<ReasoningDisclosureProps>(), {
25+
isStreaming: false,
26+
streamingLabel: "Thinking...",
27+
completeLabel: "Thoughts",
28+
defaultOpen: false,
29+
});
30+
31+
const open = ref(props.isStreaming || props.defaultOpen);
32+
33+
const lines = computed(() => cleanMarkdown(props.text).split("\n").filter(Boolean));
34+
35+
watch(() => props.isStreaming, (isStreaming) => {
36+
if (isStreaming) {
37+
open.value = true;
38+
}
39+
}, { immediate: true });
40+
41+
function cleanMarkdown(text: string): string {
42+
return text
43+
.replace(/\*\*(.+?)\*\*/g, "$1")
44+
.replace(/\*(.+?)\*/g, "$1")
45+
.replace(/`(.+?)`/g, "$1")
46+
.replace(/^#+\s+/gm, "");
47+
}
48+
</script>
49+
50+
<template>
51+
<Collapsible
52+
v-model:open="open"
53+
data-slot="reasoning-disclosure"
54+
:class="cn('relative min-w-0 w-full text-pretty *:first:mt-0 *:last:mb-0', props.class)"
55+
>
56+
<CollapsibleTrigger as-child>
57+
<Button
58+
type="button"
59+
class="group p-0! text-muted-foreground"
60+
variant="link"
61+
>
62+
{{ isStreaming ? streamingLabel : completeLabel }}
63+
<ChevronDown
64+
v-if="text.length > 0"
65+
class="size-4 transition-transform duration-200 group-data-[state=open]:rotate-180"
66+
aria-hidden="true"
67+
/>
68+
</Button>
69+
</CollapsibleTrigger>
70+
71+
<CollapsibleContent>
72+
<div
73+
v-for="(line, index) in lines"
74+
:key="index"
75+
>
76+
<span class="whitespace-pre-wrap text-sm font-normal text-muted-foreground">{{ line }}</span>
77+
</div>
78+
</CollapsibleContent>
79+
</Collapsible>
80+
</template>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as ReasoningDisclosure, type ReasoningDisclosureProps } from "./ReasoningDisclosure.vue";

content/docs/2.components/1.chat.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Unlike an integrated UI module, these registry items are installed as source cod
1616
| [ChatMessage](/docs/components/chat-message) | Individual message bubble component with content slots, leading content, and action buttons. |
1717
| [ChatPrompt](/docs/components/chat-prompt) | Prompt input wrapper for composing text areas, errors, and footer controls. |
1818
| [ChatPromptSubmit](/docs/components/chat-prompt-submit) | Status-aware submit, stop, and retry button for prompt workflows. |
19+
| [ReasoningDisclosure](/docs/components/reasoning-disclosure) | Collapsible disclosure for app-owned AI reasoning text. |
1920

2021
## Install
2122

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

36+
Install reasoning disclosure when you want to show app-owned reasoning text.
37+
38+
```bash
39+
npx shadcn-vue@latest add "https://ui.stackhacker.io/r/reasoning-disclosure.json"
40+
```
41+
3542
## Basic Composition
3643

3744
```vue
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
title: ReasoningDisclosure
3+
description: Show app-owned AI reasoning text in a compact collapsible disclosure.
4+
category: chat
5+
---
6+
7+
::component-preview
8+
---
9+
name: ReasoningDisclosureDemo
10+
---
11+
::
12+
13+
## Installation
14+
15+
```bash
16+
npx shadcn-vue@latest add "https://ui.stackhacker.io/r/reasoning-disclosure.json"
17+
```
18+
19+
## Usage
20+
21+
```vue
22+
<script setup lang="ts">
23+
import { ReasoningDisclosure } from '@/components/reasoning-disclosure'
24+
25+
const reasoning = ref('Compare the user request with the current chat state.')
26+
const isStreaming = ref(false)
27+
</script>
28+
29+
<template>
30+
<ReasoningDisclosure :text="reasoning" :is-streaming="isStreaming" />
31+
</template>
32+
```
33+
34+
## App-Owned Reasoning
35+
36+
`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.
37+
38+
The component performs light text cleanup for simple markdown markers. It is not a full markdown renderer.
39+
40+
## Examples
41+
42+
### Default
43+
44+
::component-preview
45+
---
46+
name: ReasoningDisclosureDemo
47+
---
48+
::
49+
50+
## API Reference
51+
52+
### Props
53+
54+
| Prop | Type | Default | Description |
55+
|------|------|---------|-------------|
56+
| `text` | `string` || Reasoning text supplied by the consuming app. |
57+
| `isStreaming` | `boolean` | `false` | Opens the disclosure and shows the streaming label while reasoning is streaming. |
58+
| `streamingLabel` | `string` | `'Thinking...'` | Label shown while streaming. |
59+
| `completeLabel` | `string` | `'Thoughts'` | Label shown after streaming is complete. |
60+
| `defaultOpen` | `boolean` | `false` | Initial open state when not streaming. |
61+
| `class` | `string` || Additional CSS classes. |

scripts/registry-verify.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ const expectedItems: Record<string, ExpectedRegistryItem> = {
9999
dependencies: ["@lucide/vue"],
100100
registryDependencies: ["badge", "button", "card"],
101101
},
102+
"reasoning-disclosure": {
103+
dependencies: ["@lucide/vue"],
104+
registryDependencies: ["button", "collapsible"],
105+
},
102106
};
103107

104108
if (registryIndex) {

0 commit comments

Comments
 (0)