- Accordion
- Alert
- Alert Dialog
- Autocomplete
- Auth Surface
- Avatar
- Badge
- Browse Catalog Dialog
- Button
- Card
- Checkbox
- Checkbox Group
- Collapsible
- Combobox
- Command
- Connector Setup Dialog
- Cookie Banner
- Dialog
- Directory Card
- Directory Detail
- Directory Skeleton
- DrawerНовое
- Token Parts Input
- Empty
- Field
- Fieldset
- File Preview Modal
- File Preview Skeleton
- Form
- Frame
- Group
- Icon
- Input
- Input Group
- Kbd
- Label
- Legal Shell
- Menu
- Mermaid Diagram
- Mind Map Diagram
- Not Found Screen
- Onboarding Frame
- Popover
- PDF Thumbnail
- Personalization Landing
- Preview Card
- Pricing Page
- Progress
- Radio Group
- Ring Spinner
- Scroll Area
- Select
- Separator
- Settings Page
- Settings Skills
- Settings Connectors
- Settings Capabilities
- Settings Usage
- Settings Account
- Settings Billing
- Sheet
- Sidebar
- Skeleton
- Skill Create Dialog
- Slider
- Spinner
- Stat
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Tooltip
- Компоненты AI
- Chat Conversation
- Chat Message
- Chat Response
- Chat Suggestion
- Chat Prompt Input
- Slash Highlighted Textarea
- Chat Search Dialog
- Chat Skill Doc
- Chat Connector Detail
- Chat Attachments
- Chat File Card
- Chat Token Chip
- Chat Code Block
- Chat Image
- Chat Inline Citation
- Chat Sources
- Chat Web Search
- Chat Research
- Chat Source
- Chat Actions
- Chat Context
- Chat Loader
- Chat Compaction
- Chat Timeline
- Chat Snippet
- Chat Terminal
- Chat Stack Trace
- Chat Test Results
- Chat File Tree
- Chat Environment Variables
- Chat Audio Player
- Chat Transcription
- Chat Speech Input
- Chat Mic Selector
- Chat Voice Selector
- Chat Agent
- Chat Persona
- Chat Connection
- Chat Connector Suggestion
- Chat Queue
- Chat Checkpoint
- Chat Confirmation
- Chat Artifact
- Chat JSX Preview
- Chat Schema Display
- Chat Package Info
- Chat Commit
- Chat Plan
- Chat Open In Chat
- Chat Sandbox
- Chat Model Selector
- Chat Canvas
- Chat Node
- Chat Edge
Chat Research
Панель deep-research в стиле Claude — таймлайн шагов, источники по доменам, drill-down и плашки карточки/отчёта.
Запускаю исследование. Соберу источники и подготовлю отчёт.
"use client";
import {
ChatMessage,
ChatMessageAvatar,
ChatMessageContent,
} from "@/components/ui/chat-message";
import { ChatResearchCard } from "@/components/ui/chat-research";
export default function Particle() {
return (
<div className="w-full max-w-2xl p-4">
<ChatMessage from="assistant">
<ChatMessageAvatar fallback="J" />
<ChatMessageContent>
<p>Запускаю исследование. Соберу источники и подготовлю отчёт.</p>
<ChatResearchCard
count={12}
onOpen={() => {}}
status="running"
statusLabel="Собираю источники"
title="Лучшие open-source векторные базы данных в 2025 году"
/>
</ChatMessageContent>
</ChatMessage>
</div>
);
}
Установка
pnpm dlx shadcn@latest add @oracul/chat-research
Использование
import {
ChatResearch,
ChatResearchBody,
ChatResearchByDomain,
ChatResearchHeader,
ChatResearchStatusPill,
ChatResearchStepList,
ChatResearchStepListItem,
ChatResearchTimeline,
ChatResearchTimelineStep,
} from "@/components/ui/chat-research";
<ChatResearch className="w-[28rem]">
<ChatResearchHeader
onClose={() => {}}
status={<ChatResearchStatusPill label="Gathering sources" running />}
title="Best open-source vector databases in 2025"
/>
<ChatResearchBody>
<div className="px-5 pt-1 pb-5">
<ChatResearchTimeline>
<ChatResearchTimelineStep label="Research plan created" state="done" />
<ChatResearchTimelineStep label="Gathered 24 sources" state="running">
<ChatResearchByDomain
domains={[{ domain: "github.com", count: 9 }]}
onClick={() => {}}
total={24}
/>
<ChatResearchStepList>
<ChatResearchStepListItem
count={9}
label="open-source vector database comparison"
onClick={() => {}}
/>
</ChatResearchStepList>
</ChatResearchTimelineStep>
<ChatResearchTimelineStep isLast label="Generating answer" state="pending" />
</ChatResearchTimeline>
</div>
</ChatResearchBody>
</ChatResearch>Живой ран: пилюля статуса running, таймлайн done / running / pending, источники по доменам в состоянии загрузки:
"use client";
import {
ChatResearch,
ChatResearchBody,
ChatResearchByDomain,
ChatResearchGlyph,
ChatResearchHeader,
ChatResearchStatusPill,
ChatResearchStepList,
ChatResearchStepListItem,
ChatResearchTimeline,
ChatResearchTimelineStep,
} from "@/components/ui/chat-research";
const domains = [
{ domain: "github.com", count: 7 },
{ domain: "arxiv.org", count: 4 },
];
export default function Particle() {
return (
<ChatResearch className="h-140 w-full max-w-md rounded-xl border">
<ChatResearchHeader
onClose={() => {}}
status={<ChatResearchStatusPill label="Собираю источники" running />}
title={
<span className="flex items-center gap-2">
<ChatResearchGlyph
className="size-4 text-muted-foreground"
spinning
/>
Лучшие open-source векторные базы данных в 2025 году
</span>
}
/>
<ChatResearchBody>
<div className="px-5 pt-1 pb-5">
<ChatResearchTimeline>
<ChatResearchTimelineStep
label="План исследования составлен"
state="done"
/>
<ChatResearchTimelineStep
label="Собрано 11 источников"
state="running"
>
<ChatResearchByDomain
domains={domains}
loading
onClick={() => {}}
total={11}
/>
<ChatResearchStepList>
<ChatResearchStepListItem
count={7}
label="сравнение open-source векторных баз, qdrant против weaviate, бенчмарк индекса hnsw"
onClick={() => {}}
/>
</ChatResearchStepList>
</ChatResearchTimelineStep>
<ChatResearchTimelineStep
isLast
label="Формирую ответ"
state="pending"
/>
</ChatResearchTimeline>
</div>
</ChatResearchBody>
</ChatResearch>
);
}
Завершённый ран: все точки таймлайна done, синтез-шаг — «Done», источники по доменам со строкой «more N sources»:
"use client";
import {
ChatResearch,
ChatResearchBody,
ChatResearchByDomain,
ChatResearchGlyph,
ChatResearchHeader,
ChatResearchStepList,
ChatResearchStepListItem,
ChatResearchTimeline,
ChatResearchTimelineStep,
} from "@/components/ui/chat-research";
const domains = [
{ domain: "github.com", count: 12 },
{ domain: "arxiv.org", count: 9 },
{ domain: "news.ycombinator.com", count: 6 },
{ domain: "qdrant.tech", count: 5 },
{ domain: "weaviate.io", count: 4 },
{ domain: "milvus.io", count: 3 },
{ domain: "pinecone.io", count: 3 },
];
const batches = [
{
id: "b1",
label:
"сравнение open-source векторных баз, qdrant против weaviate, бенчмарк индекса hnsw",
count: 18,
},
{
id: "b2",
label:
"масштабируемость milvus, производительность pgvector в 2025, компромиссы recall в ann",
count: 17,
},
{
id: "b3",
label:
"лицензии векторных баз, self-hosted векторный поиск, стоимость хранения эмбеддингов",
count: 12,
},
];
export default function Particle() {
return (
<ChatResearch className="h-150 w-full max-w-md rounded-xl border">
<ChatResearchHeader
onClose={() => {}}
title={
<span className="flex items-center gap-2">
<ChatResearchGlyph className="size-4 text-muted-foreground" />
Лучшие open-source векторные базы данных в 2025 году
</span>
}
/>
<ChatResearchBody>
<div className="px-5 pt-1 pb-5">
<ChatResearchTimeline>
<ChatResearchTimelineStep
label="План исследования составлен"
state="done"
/>
<ChatResearchTimelineStep
label="Собрано 47 источников"
state="done"
>
<ChatResearchByDomain
domains={domains}
onClick={() => {}}
total={47}
/>
<ChatResearchStepList>
{batches.map((batch) => (
<ChatResearchStepListItem
count={batch.count}
key={batch.id}
label={batch.label}
onClick={() => {}}
/>
))}
</ChatResearchStepList>
</ChatResearchTimelineStep>
<ChatResearchTimelineStep isLast label="Готово" state="done" />
</ChatResearchTimeline>
</div>
</ChatResearchBody>
</ChatResearch>
);
}
Плашки в ленте самодостаточны:
import {
ChatResearchCard,
ChatResearchReport,
} from "@/components/ui/chat-research";
<ChatResearchCard
count={47}
domains={["github.com", "arxiv.org", "news.ycombinator.com"]}
onOpen={() => {}}
status="completed"
statusLabel="Research complete"
title="Best open-source vector databases in 2025"
/>
<ChatResearchReport
isOpen
onDownload={() => {}}
onOpen={() => {}}
sources={47}
title="Vector databases — research report"
/>Карточка completed (favicon-стек + «Research complete · N sources») рядом с открытым отчётом (isOpen — подсветка border-ring bg-muted/40, data-state="open"):
Исследование завершено — собрал источники и подготовил отчёт.
"use client";
import {
ChatMessage,
ChatMessageAvatar,
ChatMessageContent,
} from "@/components/ui/chat-message";
import {
ChatResearchCard,
ChatResearchReport,
} from "@/components/ui/chat-research";
const topDomains = ["github.com", "arxiv.org", "news.ycombinator.com"];
export default function Particle() {
return (
<div className="flex w-full max-w-2xl flex-col gap-3 p-4">
<ChatMessage from="assistant">
<ChatMessageAvatar fallback="J" />
<ChatMessageContent>
<p>Исследование завершено — собрал источники и подготовил отчёт.</p>
<ChatResearchCard
count={47}
domains={topDomains}
onOpen={() => {}}
status="completed"
statusLabel="Исследование завершено"
title="Лучшие open-source векторные базы данных в 2025 году"
/>
<ChatResearchReport
isOpen
onDownload={() => {}}
onOpen={() => {}}
sources={47}
title="Векторные базы данных — итоговый отчёт"
/>
</ChatMessageContent>
</ChatMessage>
</div>
);
}
Закреплённая панель может предоставлять перетаскиваемый хэндл изменения ширины.
Отражай активное перетаскивание через isResizing и на хэндле, и на корне
(корень применяет select-none во время перетаскивания):
import {
ChatResearch,
ChatResearchResizeHandle,
} from "@/components/ui/chat-research";
const [resizing, setResizing] = useState(false);
<ChatResearch className="w-[28rem]" isResizing={resizing}>
<ChatResearchResizeHandle
isResizing={resizing}
onPointerDown={(event) => {
setResizing(true);
// track event.clientX, update --preview-width, then setResizing(false)
}}
/>
{/* header + body */}
</ChatResearch>Хэндл изменения ширины: в покое — прозрачная линия с капсулой, при захвате (isResizing) линия bg-info + бордер капсулы border-info, тело панели select-none:
"use client";
import { useState } from "react";
import {
ChatResearch,
ChatResearchBody,
ChatResearchByDomain,
ChatResearchGlyph,
ChatResearchHeader,
ChatResearchResizeHandle,
ChatResearchStatusPill,
ChatResearchStepList,
ChatResearchStepListItem,
ChatResearchTimeline,
ChatResearchTimelineStep,
} from "@/components/ui/chat-research";
const domains = [
{ domain: "github.com", count: 9 },
{ domain: "arxiv.org", count: 6 },
{ domain: "news.ycombinator.com", count: 4 },
];
export default function Particle() {
const [resizing, setResizing] = useState(false);
return (
<ChatResearch
className="h-140 w-full max-w-md rounded-xl border"
isResizing={resizing}
>
<ChatResearchResizeHandle
isResizing={resizing}
onPointerDown={() => {
setResizing(true);
const stop = () => {
setResizing(false);
window.removeEventListener("pointerup", stop);
};
window.addEventListener("pointerup", stop);
}}
/>
<ChatResearchHeader
onClose={() => {}}
status={<ChatResearchStatusPill label="Сбор источников" running />}
title={
<span className="flex items-center gap-2">
<ChatResearchGlyph
className="size-4 text-muted-foreground"
spinning
strokeWidth={1.5}
/>
Лучшие open-source векторные базы данных в 2025 году
</span>
}
/>
<ChatResearchBody>
<div className="px-5 pt-1 pb-5">
<ChatResearchTimeline>
<ChatResearchTimelineStep
label="План исследования создан"
state="done"
/>
<ChatResearchTimelineStep
label="Собрано 19 источников"
state="running"
>
<ChatResearchByDomain
domains={domains}
onClick={() => {}}
total={19}
/>
<ChatResearchStepList>
<ChatResearchStepListItem
count={9}
label="сравнение open-source векторных баз данных, qdrant против weaviate, бенчмарк индекса hnsw"
onClick={() => {}}
/>
</ChatResearchStepList>
</ChatResearchTimelineStep>
<ChatResearchTimelineStep
isLast
label="Формирование ответа"
state="pending"
/>
</ChatResearchTimeline>
</div>
</ChatResearchBody>
</ChatResearch>
);
}
Drill-down переиспользует <ChatSource> из @oracul/chat-sources для каждой строки результата:
import { ChatSource } from "@/components/ui/chat-sources";
import {
ChatResearchSourceGroup,
ChatResearchSourceGroups,
} from "@/components/ui/chat-research";
<ChatResearchSourceGroups>
<ChatResearchSourceGroup count={2} label="vector database benchmarks">
<ChatSource
href="https://github.com/qdrant/qdrant"
hostname="github.com"
title="Qdrant"
/>
<ChatSource
href="https://arxiv.org/abs/2401.12345"
hostname="arxiv.org"
title="ANN benchmarks"
/>
</ChatResearchSourceGroup>
</ChatResearchSourceGroups>Drill-режим: шапка с onBack (шеврон назад + счётчик источников), раскрытые подгруппы и пустая группа (count={0} → emptyLabel):
"use client";
import {
ChatResearch,
ChatResearchBody,
ChatResearchHeader,
ChatResearchSourceGroup,
ChatResearchSourceGroups,
} from "@/components/ui/chat-research";
import { ChatSource } from "@/components/ui/chat-sources";
const groups = [
{
id: "g1",
label: "сравнение open-source векторных баз данных",
sources: [
{ title: "Qdrant", href: "https://github.com/qdrant/qdrant" },
{ title: "Weaviate", href: "https://github.com/weaviate/weaviate" },
{ title: "Milvus", href: "https://milvus.io/docs" },
],
},
{
id: "g2",
label: "бенчмарк индекса HNSW",
sources: [
{ title: "Бенчмарки ANN", href: "https://arxiv.org/abs/2401.12345" },
{
title: "Обсуждение HNSW против IVF",
href: "https://news.ycombinator.com/item?id=39000000",
},
],
},
{
id: "g3",
label: "производительность pgvector в 2025 году",
sources: [],
},
];
function hostname(href: string): string {
try {
return new URL(href).hostname.replace(/^www\./, "");
} catch {
return "";
}
}
export default function Particle() {
return (
<ChatResearch className="h-140 w-full max-w-md rounded-xl border">
<ChatResearchHeader
onBack={() => {}}
onClose={() => {}}
title="47 источников"
/>
<ChatResearchBody>
<div className="px-2 pt-1 pb-5">
<ChatResearchSourceGroups>
{groups.map((group) => (
<ChatResearchSourceGroup
count={group.sources.length}
key={group.id}
label={group.label}
>
{group.sources.map((source) => (
<ChatSource
href={source.href}
hostname={hostname(source.href)}
key={source.href}
title={source.title}
/>
))}
</ChatResearchSourceGroup>
))}
</ChatResearchSourceGroups>
</div>
</ChatResearchBody>
</ChatResearch>
);
}
Состояния
| Состояние | Где | Как выглядит |
|---|---|---|
run.running | ChatResearchStatusPill status="running" + glyph spinning | пилюля bg-primary/10, glyph крутится, майлстон сбора state="running" (пульсирует) |
run.completed | ChatResearchStatusPill status="completed" (или скрыта) | все точки таймлайна done, glyph статичен, синтез-шаг — «Done» |
run.error | ChatResearchStatusPill status="error" + ChatResearchCard status="error" | пилюля bg-destructive/10 text-destructive с иконкой TriangleAlert; карточка показывает иконку и подпись в text-destructive; майлстон ошибки — state="error" |
timeline pending | ChatResearchTimelineStep state="pending" | полая точка (border + bg-card), подпись muted-foreground |
timeline running | state="running" | залитая bg-primary точка, пульсация, подпись foreground |
timeline done | state="done" | залитая bg-muted-foreground точка, подпись foreground |
timeline error | state="error" | иконка TriangleAlert text-destructive вместо точки, подпись text-destructive |
| rail | non-last rows | непрерывная линия bg-border; на последнем шаге (isLast) отсутствует |
by-domain loading пустой | loading, нет доменов | 3 skeleton-бара |
by-domain loading частичный | loading + домены | бары + хвостовой skeleton |
by-domain hasMore | доменов больше top | строка «more N sources» с многоточием |
| by-domain статичный/интерактивный | без / с onClick | div vs button с hover:border-foreground/25 |
| source group раскрыт/свёрнут | defaultOpen / клик | список результатов / шеврон повёрнут |
| source group пустой | count={0} | сообщение emptyLabel |
card running | status="running" | спиннер + «Gathering sources · N sources» |
card completed | status="completed" | favicon-стек + «Research complete · N sources» |
card error | status="error" | иконка TriangleAlert + подпись в text-destructive |
report isOpen | isOpen | подсветка border-ring bg-muted/40, data-state="open" |
| favicon | загружен / ошибка | <img> / глобус-фоллбэк |
| header root/drill | без / с onBack | только заголовок / шеврон назад + счётчик источников |
| resize handle idle | ChatResearchResizeHandle без drag | прозрачная линия + видимая капсула с дефолтным бордером |
| resize handle hover | наведение на хэндл | линия и бордер капсулы тонируются в info |
| resize handle active | isResizing / захват | линия bg-info + бордер капсулы border-info; тело панели select-none |
| focus-visible | Tab на любой кнопке | кольцо ring-2 ring-ring ring-offset-1 |
Состояние ошибки: карточка status="error" (иконка TriangleAlert + подпись text-destructive), пилюля ChatResearchStatusPill status="error" и майлстон таймлайна state="error":
Не удалось завершить исследование — источники недоступны.
"use client";
import {
ChatMessage,
ChatMessageAvatar,
ChatMessageContent,
} from "@/components/ui/chat-message";
import {
ChatResearch,
ChatResearchBody,
ChatResearchCard,
ChatResearchGlyph,
ChatResearchHeader,
ChatResearchStatusPill,
ChatResearchTimeline,
ChatResearchTimelineStep,
} from "@/components/ui/chat-research";
export default function Particle() {
return (
<div className="flex w-full max-w-2xl flex-col gap-6 p-4">
<ChatMessage from="assistant">
<ChatMessageAvatar fallback="J" />
<ChatMessageContent>
<p>Не удалось завершить исследование — источники недоступны.</p>
<ChatResearchCard
count={6}
onOpen={() => {}}
status="error"
statusLabel="Исследование не удалось"
title="Лучшие open-source векторные базы данных в 2025 году"
/>
</ChatMessageContent>
</ChatMessage>
<ChatResearch className="h-105 w-full max-w-md rounded-xl border">
<ChatResearchHeader
onClose={() => {}}
status={<ChatResearchStatusPill label="Не удалось" status="error" />}
title={
<span className="flex items-center gap-2">
<ChatResearchGlyph className="size-4 text-muted-foreground" />
Лучшие open-source векторные базы данных в 2025 году
</span>
}
/>
<ChatResearchBody>
<div className="px-5 pt-1 pb-5">
<ChatResearchTimeline>
<ChatResearchTimelineStep
label="План исследования создан"
state="done"
/>
<ChatResearchTimelineStep
label="Собрано 6 источников"
state="done"
/>
<ChatResearchTimelineStep
isLast
label="Исследование не удалось — источники недоступны"
state="error"
/>
</ChatResearchTimeline>
</div>
</ChatResearchBody>
</ChatResearch>
</div>
);
}
API
| Компонент | Описание |
|---|---|
ChatResearch | Корень панели <aside>, ширину задаёт потребитель через className; isResizing включает select-none. |
ChatResearchResizeHandle | Грабер изменения ширины: onPointerDown, isResizing (линия + бордер капсулы тонируются в info). |
ChatResearchHeader | Шапка: title, onBack (drill), onClose, слот status. |
ChatResearchStatusPill | Phase-пилюля; status (running / completed / error) задаёт спиннер + bg-primary/10, иконку ошибки + bg-destructive/10, либо muted. |
ChatResearchBody | Скролл-контейнер (поверх ScrollArea). |
ChatResearchTimeline / ChatResearchTimelineStep | Таймлайн и шаг; state, isLast, вложенные children. |
ChatResearchStepList / ChatResearchStepListItem | Список пачек запросов; serif-label + count, onClick проваливается. |
ChatResearchByDomain | Источники по доменам: бары, skeleton-ы, строка «more». |
ChatResearchSourceGroups / ChatResearchSourceGroup | Drill-down подгруппы; в children — <ChatSource>. |
ChatResearchFavicon / ChatResearchFaviconStack | Favicon домена с фоллбэком на глобус и стек. |
ChatResearchGlyph | Чистый SVG-знак; spinning для рана. |
ChatResearchCard | In-feed карточка исследования. |
ChatResearchReport | In-feed плашка отчёта-файла. |
На этой странице