Skip to content
edmi-ui

Chain of Thought

Step-by-step reasoning timeline with search results and images per step.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Searched for NVDAx news
reuters.comsec.govnasdaq.com
Read the 30-day chart

NVDAx 30-day price

Comparing weights with the mandate
MAG4 · drift limit 5%
Write the answer
Searched for NVDAx news
reuters.com
sec.gov
nasdaq.com
Read the 30-day chart

NVDAx 30-day price

Comparing weights with the mandate
MAG4 · drift limit 5%
Write the answer
Searched for NVDAx news
reuters.comsec.govnasdaq.com
Read the 30-day chart

NVDAx 30-day price

Comparing weights with the mandate
MAG4 · drift limit 5%
Write the answer

Built on badge and collapsible.

Terminal window
npx shadcn@latest add @edmi-ui/ai-chain-of-thought
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/ai-chain-of-thought
Terminal window
yarn dlx shadcn@latest add @edmi-ui/ai-chain-of-thought
Terminal window
bunx shadcn@latest add @edmi-ui/ai-chain-of-thought

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/ai-chain-of-thought
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-chain-of-thought
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-chain-of-thought
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-chain-of-thought

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-chain-of-thought.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-chain-of-thought.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-chain-of-thought.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-chain-of-thought.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
ChainOfThought,
ChainOfThoughtHeader,
} from "@/components/ai/chain-of-thought";
<ChainOfThought defaultOpen className="max-w-lg">
<ChainOfThoughtHeader />
{/* … */}
</ChainOfThought>
<script setup lang="ts">
import {
ChainOfThought,
ChainOfThoughtContent,
ChainOfThoughtHeader,
ChainOfThoughtImage,
ChainOfThoughtSearchResult,
ChainOfThoughtSearchResults,
ChainOfThoughtStep,
} from "@/components/ai/chain-of-thought";
import { Shimmer } from "@/components/ai/shimmer";
</script>
<template>
<ChainOfThought default-open class="max-w-lg">
<ChainOfThoughtHeader />
<ChainOfThoughtContent>
<ChainOfThoughtStep :icon="SearchIcon" label="Searched for NVDAx news">
<ChainOfThoughtSearchResults>
<ChainOfThoughtSearchResult v-for="site in ['reuters.com', 'sec.gov', 'nasdaq.com']" :key="site">
<GlobeIcon class="size-3" />
{{ site }}
</ChainOfThoughtSearchResult>
</ChainOfThoughtSearchResults>
</ChainOfThoughtStep>
<ChainOfThoughtStep :icon="ChartLineIcon" label="Read the 30-day chart">
<ChainOfThoughtImage caption="NVDAx 30-day price">
<div class="h-28 w-64 bg-linear-to-br from-chart-2 to-chart-1" />
</ChainOfThoughtImage>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="active" :icon="SparklesIcon" description="MAG4 · drift limit 5%">
<template #label>
<Shimmer as="span">Comparing weights with the mandate</Shimmer>
</template>
</ChainOfThoughtStep>
<ChainOfThoughtStep status="pending" label="Write the answer" />
</ChainOfThoughtContent>
</ChainOfThought>
</template>
<script lang="ts">
import {
ChainOfThought,
ChainOfThoughtContent,
ChainOfThoughtHeader,
ChainOfThoughtImage,
ChainOfThoughtSearchResult,
ChainOfThoughtSearchResults,
ChainOfThoughtStep,
} from "$lib/components/ai/chain-of-thought";
</script>
<ChainOfThought defaultOpen class="max-w-lg">
<ChainOfThoughtHeader />
<ChainOfThoughtContent>
<ChainOfThoughtStep label="Searched for NVDAx news">
<ChainOfThoughtSearchResults>
{#each sites as site (site)}
<ChainOfThoughtSearchResult>{site}</ChainOfThoughtSearchResult>
{/each}
</ChainOfThoughtSearchResults>
</ChainOfThoughtStep>
<ChainOfThoughtStep label="Read the 30-day chart">
<ChainOfThoughtImage caption="NVDAx 30-day price">
<div class="h-28 w-64 bg-linear-to-br from-chart-2 to-chart-1"></div>
</ChainOfThoughtImage>
</ChainOfThoughtStep>
<ChainOfThoughtStep
status="active"
description="MAG4 · drift limit 5%"
/>
<ChainOfThoughtStep status="pending" label="Write the answer" />
</ChainOfThoughtContent>
</ChainOfThought>
registry/components/ai/chain-of-thought.tsx
"use client";
import { cn } from "cn";
import type { ComponentProps, ReactNode } from "react";
import { createContext, memo, useContext, useMemo } from "react";
import { BrainIcon, CaretDownIcon } from "@phosphor-icons/react";
import { useControllableState } from "@/registry/edmi/hooks/ai/use-controllable-state";
import { Badge } from "@/registry/edmi/ui/badge";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/registry/edmi/ui/collapsible";
interface ChainOfThoughtContextValue {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
}
const ChainOfThoughtContext = createContext<ChainOfThoughtContextValue | null>(
null,
);
const useChainOfThought = () => {
const context = useContext(ChainOfThoughtContext);
if (!context) {
throw new Error(
"ChainOfThought components must be used within ChainOfThought",
);
}
return context;
};
export type ChainOfThoughtProps = ComponentProps<"div"> & {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
};
export const ChainOfThought = memo(
({
className,
open,
defaultOpen = false,
onOpenChange,
children,
...props
}: ChainOfThoughtProps) => {
const [isOpen, setIsOpen] = useControllableState({
defaultProp: defaultOpen,
onChange: onOpenChange,
prop: open,
});
const chainOfThoughtContext = useMemo(
() => ({ isOpen, setIsOpen }),
[isOpen, setIsOpen],
);
return (
<ChainOfThoughtContext.Provider value={chainOfThoughtContext}>
<div
data-slot="ai-chain-of-thought"
className={cn("not-prose w-full space-y-3", className)}
{...props}
>
{children}
</div>
</ChainOfThoughtContext.Provider>
);
},
);
export type ChainOfThoughtHeaderProps = ComponentProps<
typeof CollapsibleTrigger
>;
export const ChainOfThoughtHeader = memo(
({ className, children, ...props }: ChainOfThoughtHeaderProps) => {
const { isOpen, setIsOpen } = useChainOfThought();
return (
<Collapsible onOpenChange={setIsOpen} open={isOpen}>
<CollapsibleTrigger
data-slot="ai-chain-of-thought-header"
className={cn(
"flex w-full items-center gap-2 text-[13.5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
className,
)}
{...props}
>
<BrainIcon
className="size-4"
/>
<span className="flex-1 text-left">
{children ?? "Chain of thought"}
</span>
<CaretDownIcon
className={cn(
"size-4 transition-transform",
isOpen ? "rotate-180" : "rotate-0",
)}
/>
</CollapsibleTrigger>
</Collapsible>
);
},
);
export type ChainOfThoughtStepProps = Omit<ComponentProps<"div">, "title"> & {
/** Any element (an icon, an svg). Defaults to a small dot. */
icon?: ReactNode;
label: ReactNode;
description?: ReactNode;
status?: "complete" | "active" | "pending";
};
// Icon colour and label colour per status (board AI 02): complete = muted icon, soft label; active = brand icon,
// foreground label; pending = faint.
const stepIconStyles = {
active: "text-brand",
complete: "text-muted-foreground",
pending: "text-muted-foreground-2",
};
const stepLabelStyles = {
active: "text-foreground",
complete: "text-foreground-2",
pending: "text-muted-foreground-2",
};
export const ChainOfThoughtStep = memo(
({
className,
icon,
label,
description,
status = "complete",
children,
...props
}: ChainOfThoughtStepProps) => (
<div
data-slot="ai-chain-of-thought-step"
data-status={status}
className={cn(
"group/step relative grid grid-cols-[22px_1fr] gap-2.5 pb-3.5 text-[13.5px] last:pb-0",
className,
)}
{...props}
>
<span
className={cn(
"relative z-10 inline-flex size-[22px] items-center justify-center rounded-full bg-card [&_svg]:size-3.5",
stepIconStyles[status],
)}
>
{icon ?? <span className="size-1.5 rounded-full bg-current" />}
</span>
<span className="absolute top-6 -bottom-2 left-[10.5px] w-px bg-border group-last/step:hidden" />
<div className="min-w-0 space-y-2">
<div className={stepLabelStyles[status]}>{label}</div>
{description && (
<div className="-mt-1.5 text-xs text-muted-foreground">
{description}
</div>
)}
{children}
</div>
</div>
),
);
export type ChainOfThoughtSearchResultsProps = ComponentProps<"div">;
export const ChainOfThoughtSearchResults = memo(
({ className, ...props }: ChainOfThoughtSearchResultsProps) => (
<div
data-slot="ai-chain-of-thought-search-results"
className={cn("flex flex-wrap items-center gap-1.5", className)}
{...props}
/>
),
);
export type ChainOfThoughtSearchResultProps = ComponentProps<typeof Badge>;
export const ChainOfThoughtSearchResult = memo(
({ className, children, ...props }: ChainOfThoughtSearchResultProps) => (
<Badge
data-slot="ai-chain-of-thought-search-result"
className={cn(
"h-5 gap-1 px-[7px] text-[11.5px] font-normal text-foreground-2",
className,
)}
shape="pill"
variant="secondary"
{...props}
>
{children}
</Badge>
),
);
export type ChainOfThoughtContentProps = ComponentProps<
typeof CollapsibleContent
>;
export const ChainOfThoughtContent = memo(
({ className, children, ...props }: ChainOfThoughtContentProps) => {
const { isOpen } = useChainOfThought();
return (
<Collapsible open={isOpen}>
<CollapsibleContent
data-slot="ai-chain-of-thought-content"
className={cn(
"h-(--collapsible-panel-height) overflow-hidden outline-none transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0",
className,
)}
{...props}
>
<div className="pl-0.5">{children}</div>
</CollapsibleContent>
</Collapsible>
);
},
);
export type ChainOfThoughtImageProps = ComponentProps<"div"> & {
caption?: string;
};
export const ChainOfThoughtImage = memo(
({ className, children, caption, ...props }: ChainOfThoughtImageProps) => (
<div
data-slot="ai-chain-of-thought-image"
className={cn("w-fit max-w-full space-y-1", className)}
{...props}
>
<div className="relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-[10px] border border-border bg-muted">
{children}
</div>
{caption && <p className="text-xs text-muted-foreground">{caption}</p>}
</div>
),
);
ChainOfThought.displayName = "ChainOfThought";
ChainOfThoughtHeader.displayName = "ChainOfThoughtHeader";
ChainOfThoughtStep.displayName = "ChainOfThoughtStep";
ChainOfThoughtSearchResults.displayName = "ChainOfThoughtSearchResults";
ChainOfThoughtSearchResult.displayName = "ChainOfThoughtSearchResult";
ChainOfThoughtContent.displayName = "ChainOfThoughtContent";
ChainOfThoughtImage.displayName = "ChainOfThoughtImage";
registry/components/ai/chain-of-thought/ChainOfThought.vue
<script setup lang="ts">
import type { HTMLAttributes, Ref } from "vue"
import { useVModel } from "@vueuse/core"
import { provide } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { ChainOfThoughtKey } from "./context"
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
open?: boolean
defaultOpen?: boolean
}>(), {
open: undefined,
defaultOpen: false,
})
const emit = defineEmits<{
(e: "update:open", value: boolean): void
}>()
const isOpen = useVModel(props, "open", emit, {
defaultValue: props.defaultOpen,
passive: true,
}) as Ref<boolean>
provide(ChainOfThoughtKey, {
isOpen,
setIsOpen: (value: boolean) => {
isOpen.value = value
},
})
</script>
<template>
<div data-slot="ai-chain-of-thought" :class="cn('not-prose w-full space-y-3', props.class)">
<slot />
</div>
</template>
registry/components/ai/chain-of-thought/ChainOfThoughtContent.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Collapsible, CollapsibleContent } from "@/registry/edmi/ui/collapsible"
import { useChainOfThoughtContext } from "./context"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { isOpen } = useChainOfThoughtContext()
</script>
<template>
<Collapsible :open="isOpen">
<CollapsibleContent
data-slot="ai-chain-of-thought-content"
:class="cn(
'overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down',
props.class,
)"
>
<div class="pl-0.5">
<slot />
</div>
</CollapsibleContent>
</Collapsible>
</template>
registry/components/ai/chain-of-thought/ChainOfThoughtHeader.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { ChevronDownIcon, SparklesIcon } from "@lucide/vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Collapsible, CollapsibleTrigger } from "@/registry/edmi/ui/collapsible"
import { useChainOfThoughtContext } from "./context"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { isOpen, setIsOpen } = useChainOfThoughtContext()
</script>
<template>
<Collapsible :open="isOpen" @update:open="setIsOpen">
<CollapsibleTrigger
data-slot="ai-chain-of-thought-header"
:class="cn(
'flex w-full items-center gap-2 text-[13.5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring',
props.class,
)"
>
<SparklesIcon class="size-4" />
<span class="flex-1 text-left"><slot>Chain of thought</slot></span>
<ChevronDownIcon
:class="cn('size-4 transition-transform', isOpen ? 'rotate-180' : 'rotate-0')"
/>
</CollapsibleTrigger>
</Collapsible>
</template>
registry/components/ai/chain-of-thought/ChainOfThoughtImage.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
caption?: string
}>()
</script>
<template>
<div data-slot="ai-chain-of-thought-image" :class="cn('w-fit max-w-full space-y-1', props.class)">
<div class="relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-[10px] border border-border bg-muted">
<slot />
</div>
<p v-if="caption" class="text-xs text-muted-foreground">
{{ caption }}
</p>
</div>
</template>
registry/components/ai/chain-of-thought/ChainOfThoughtSearchResult.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Badge } from "@/registry/edmi/ui/badge"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<Badge
data-slot="ai-chain-of-thought-search-result"
shape="pill"
variant="secondary"
:class="cn('h-5 gap-1 px-[7px] text-[11.5px] font-normal text-foreground-2', props.class)"
>
<slot />
</Badge>
</template>
registry/components/ai/chain-of-thought/ChainOfThoughtSearchResults.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div data-slot="ai-chain-of-thought-search-results" :class="cn('flex flex-wrap items-center gap-1.5', props.class)">
<slot />
</div>
</template>
registry/components/ai/chain-of-thought/ChainOfThoughtStep.vue
<script setup lang="ts">
import type { Component, HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
const props = withDefaults(defineProps<{
class?: HTMLAttributes["class"]
/** Icon component; the `icon` slot works too. Defaults to a small dot. */
icon?: Component
label?: string
description?: string
status?: "complete" | "active" | "pending"
}>(), {
status: "complete",
})
// Icon colour and label colour per status (board AI 02): complete = muted icon, soft label; active = brand icon,
// foreground label; pending = faint.
const iconStyles = {
active: "text-brand",
complete: "text-muted-foreground",
pending: "text-muted-foreground-2",
}
const labelStyles = {
active: "text-foreground",
complete: "text-foreground-2",
pending: "text-muted-foreground-2",
}
</script>
<template>
<div
data-slot="ai-chain-of-thought-step"
:data-status="status"
:class="cn(
'group/step relative grid grid-cols-[22px_1fr] gap-2.5 pb-3.5 text-[13.5px] last:pb-0',
props.class,
)"
>
<span
:class="cn(
'relative z-10 inline-flex size-[22px] items-center justify-center rounded-full bg-card [&_svg]:size-3.5',
iconStyles[status],
)"
>
<slot name="icon">
<component :is="icon" v-if="icon" />
<span v-else class="size-1.5 rounded-full bg-current" />
</slot>
</span>
<span class="absolute top-6 -bottom-2 left-[10.5px] w-px bg-border group-last/step:hidden" />
<div class="min-w-0 space-y-2">
<div :class="labelStyles[status]">
<slot name="label">
{{ label }}
</slot>
</div>
<div v-if="description || $slots.description" class="-mt-1.5 text-xs text-muted-foreground">
<slot name="description">
{{ description }}
</slot>
</div>
<slot />
</div>
</div>
</template>
registry/components/ai/chain-of-thought/context.ts
import type { InjectionKey, Ref } from "vue";
import { inject } from "vue";
export interface ChainOfThoughtContextValue {
isOpen: Ref<boolean>;
setIsOpen: (open: boolean) => void;
}
export const ChainOfThoughtKey: InjectionKey<ChainOfThoughtContextValue> =
Symbol("ChainOfThoughtContext");
export function useChainOfThoughtContext() {
const ctx = inject(ChainOfThoughtKey);
if (!ctx)
throw new Error(
"ChainOfThought components must be used within <ChainOfThought>",
);
return ctx;
}
registry/components/ai/chain-of-thought/index.ts
export { default as ChainOfThought } from "./ChainOfThought.vue";
export { default as ChainOfThoughtContent } from "./ChainOfThoughtContent.vue";
export { default as ChainOfThoughtHeader } from "./ChainOfThoughtHeader.vue";
export { default as ChainOfThoughtImage } from "./ChainOfThoughtImage.vue";
export { default as ChainOfThoughtSearchResult } from "./ChainOfThoughtSearchResult.vue";
export { default as ChainOfThoughtSearchResults } from "./ChainOfThoughtSearchResults.vue";
export { default as ChainOfThoughtStep } from "./ChainOfThoughtStep.vue";
export * from "./context";
src/lib/registry/ai/chain-of-thought/chain-of-thought-content.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Collapsible, CollapsibleContent } from "$lib/registry/ui/collapsible/index.js";
import type { ComponentProps } from "svelte";
import { useChainOfThought } from "./use-chain-of-thought.svelte.js";
let {
class: className,
children,
...restProps
}: ComponentProps<typeof CollapsibleContent> = $props();
const cot = useChainOfThought();
</script>
<Collapsible open={cot.isOpen}>
<CollapsibleContent
data-slot="ai-chain-of-thought-content"
class={cn(
"overflow-hidden outline-none data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
className
)}
{...restProps}
>
<div class="pl-0.5">
{@render children?.()}
</div>
</CollapsibleContent>
</Collapsible>
src/lib/registry/ai/chain-of-thought/chain-of-thought-header.svelte
<script lang="ts">
import BrainIcon from "phosphor-svelte/lib/Brain";
import CaretDownIcon from "phosphor-svelte/lib/CaretDown";
import { cn } from "$lib/utils.js";
import { Collapsible, CollapsibleTrigger } from "$lib/registry/ui/collapsible/index.js";
import type { ComponentProps } from "svelte";
import { useChainOfThought } from "./use-chain-of-thought.svelte.js";
let {
class: className,
children,
...restProps
}: ComponentProps<typeof CollapsibleTrigger> = $props();
const cot = useChainOfThought();
</script>
<Collapsible open={cot.isOpen} onOpenChange={cot.setIsOpen}>
<CollapsibleTrigger
data-slot="ai-chain-of-thought-header"
class={cn(
"flex w-full items-center gap-2 text-[13.5px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
className
)}
{...restProps}
>
<BrainIcon
class="size-4"
/>
<span class="flex-1 text-left">
{#if children}{@render children()}{:else}Chain of thought{/if}
</span>
<CaretDownIcon
class={cn("size-4 transition-transform", cot.isOpen ? "rotate-180" : "rotate-0")}
/>
</CollapsibleTrigger>
</Collapsible>
src/lib/registry/ai/chain-of-thought/chain-of-thought-image.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
caption,
children,
...restProps
}: HTMLAttributes<HTMLDivElement> & { caption?: string } = $props();
</script>
<div data-slot="ai-chain-of-thought-image" class={cn("w-fit max-w-full space-y-1", className)} {...restProps}>
<div
class="relative flex max-h-[22rem] items-center justify-center overflow-hidden rounded-[10px] border border-border bg-muted"
>
{@render children?.()}
</div>
{#if caption}
<p class="text-xs text-muted-foreground">{caption}</p>
{/if}
</div>
src/lib/registry/ai/chain-of-thought/chain-of-thought-search-result.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Badge } from "$lib/registry/ui/badge/index.js";
import type { ComponentProps } from "svelte";
let { class: className, children, ...restProps }: ComponentProps<typeof Badge> = $props();
</script>
<Badge
data-slot="ai-chain-of-thought-search-result"
shape="pill"
variant="secondary"
class={cn("h-5 gap-1 px-[7px] text-[11.5px] font-normal text-foreground-2", className)}
{...restProps}
>
{@render children?.()}
</Badge>
src/lib/registry/ai/chain-of-thought/chain-of-thought-search-results.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div
data-slot="ai-chain-of-thought-search-results"
class={cn("flex flex-wrap items-center gap-1.5", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ai/chain-of-thought/chain-of-thought-step.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
let {
class: className,
icon,
label,
description,
status = "complete",
children,
...restProps
}: Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
/** Snippet that renders the icon (an `IconPlaceholder`, an svg). Defaults to a small dot. */
icon?: Snippet;
label?: string | Snippet;
description?: string | Snippet;
status?: "complete" | "active" | "pending";
children?: Snippet;
} = $props();
// Icon colour and label colour per status (board AI 02): complete = muted icon, soft label; active = brand icon,
// foreground label; pending = faint.
const iconStyles = {
active: "text-brand",
complete: "text-muted-foreground",
pending: "text-muted-foreground-2",
};
const labelStyles = {
active: "text-foreground",
complete: "text-foreground-2",
pending: "text-muted-foreground-2",
};
</script>
<div
data-slot="ai-chain-of-thought-step"
data-status={status}
class={cn(
"group/step relative grid grid-cols-[22px_1fr] gap-2.5 pb-3.5 text-[13.5px] last:pb-0",
className
)}
{...restProps}
>
<span
class={cn(
"relative z-10 inline-flex size-[22px] items-center justify-center rounded-full bg-card [&_svg]:size-3.5",
iconStyles[status]
)}
>
{#if icon}
{@render icon()}
{:else}
<span class="size-1.5 rounded-full bg-current"></span>
{/if}
</span>
<span class="absolute top-6 -bottom-2 left-[10.5px] w-px bg-border group-last/step:hidden"></span>
<div class="min-w-0 space-y-2">
<div class={labelStyles[status]}>
{#if typeof label === "string"}{label}{:else if label}{@render label()}{/if}
</div>
{#if description}
<div class="-mt-1.5 text-xs text-muted-foreground">
{#if typeof description === "string"}{description}{:else}{@render description()}{/if}
</div>
{/if}
{@render children?.()}
</div>
</div>
src/lib/registry/ai/chain-of-thought/chain-of-thought.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { setChainOfThoughtContext } from "./use-chain-of-thought.svelte.js";
let {
class: className,
open = $bindable(),
defaultOpen = false,
children,
...restProps
}: HTMLAttributes<HTMLDivElement> & { open?: boolean; defaultOpen?: boolean } = $props();
// svelte-ignore state_referenced_locally
if (open === undefined) open = defaultOpen;
setChainOfThoughtContext({
get isOpen() {
return !!open;
},
setIsOpen: (value: boolean) => {
open = value;
},
});
</script>
<div data-slot="ai-chain-of-thought" class={cn("not-prose w-full space-y-3", className)} {...restProps}>
{@render children?.()}
</div>
src/lib/registry/ai/chain-of-thought/index.ts
import Root from "./chain-of-thought.svelte";
import Content from "./chain-of-thought-content.svelte";
import Header from "./chain-of-thought-header.svelte";
import Image from "./chain-of-thought-image.svelte";
import SearchResult from "./chain-of-thought-search-result.svelte";
import SearchResults from "./chain-of-thought-search-results.svelte";
import Step from "./chain-of-thought-step.svelte";
export * from "./use-chain-of-thought.svelte.js";
export {
Content,
Content as ChainOfThoughtContent,
Header,
Header as ChainOfThoughtHeader,
Image,
Image as ChainOfThoughtImage,
Root,
//
Root as ChainOfThought,
SearchResult,
SearchResult as ChainOfThoughtSearchResult,
SearchResults,
SearchResults as ChainOfThoughtSearchResults,
Step,
Step as ChainOfThoughtStep,
};
src/lib/registry/ai/chain-of-thought/use-chain-of-thought.svelte.ts
import { getContext, setContext } from "svelte";
/** Getter object so consumers stay reactive. */
export interface ChainOfThoughtContextValue {
readonly isOpen: boolean;
setIsOpen: (open: boolean) => void;
}
const KEY = Symbol("ai-chain-of-thought");
export function setChainOfThoughtContext(value: ChainOfThoughtContextValue) {
return setContext(KEY, value);
}
export function useChainOfThought(): ChainOfThoughtContextValue {
const ctx = getContext<ChainOfThoughtContextValue | undefined>(KEY);
if (!ctx)
throw new Error(
"ChainOfThought components must be used within <ChainOfThought>",
);
return ctx;
}
Part Prop Type Default
ChainOfThought open, defaultOpen, onOpenChange controlled / uncontrolled open state defaultOpen = false
ChainOfThoughtHeader children label Chain of thought
ChainOfThoughtStep icon ReactNode (an IconPlaceholder or svg) small dot
ChainOfThoughtStep label, description ReactNode
ChainOfThoughtStep status complete | active | pending complete
ChainOfThoughtSearchResult Badge props secondary pill
ChainOfThoughtImage caption string
  • icon takes an element instead of an icon component, so it follows the consumer’s iconLibrary.
  • Statuses: active shows a --brand icon and a foreground label (pair it with Shimmer), pending is faint.
  • A 1px --border line joins the step icons and stops after the last step.