Skip to content
Draft
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
7 changes: 5 additions & 2 deletions src/components/shared/ComponentDetail/ComponentDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,15 @@ interface ComponentDetailProps {
* top nav). In `stacked` layout this caps the inline source card's height.
*/
sourcePanelHeight?: string;
/** Hide the source-authored description when the caller renders its own description panel. */
hideDescription?: boolean;
}

export const ComponentDetail = ({
reference,
layout = "split",
sourcePanelHeight,
hideDescription = false,
}: ComponentDetailProps) => {
const hydrated = useHydrateComponentReference(reference);

Expand Down Expand Up @@ -285,7 +288,7 @@ export const ComponentDetail = ({
// their intrinsic width, which looks broken in wide panes.
<BlockStack gap="6" align="stretch">
{header}
{description}
{!hideDescription && description}
{githubLinks}
{io}
{hydrated.text && (
Expand Down Expand Up @@ -313,7 +316,7 @@ export const ComponentDetail = ({
<InlineStack gap="6" blockAlign="start">
<div className="flex-2 min-w-0 flex flex-col gap-4">
{header}
{description}
{!hideDescription && description}
{githubLinks}
{io}
</div>
Expand Down
8 changes: 8 additions & 0 deletions src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,12 @@ export const ExistingFlags: ConfigFlags = {
default: false,
category: "beta",
},

["component-search-v2-ai-descriptions"]: {
name: "Auto-generate Components V2 AI descriptions",
description:
"Automatically generate an AI description when viewing a component in Components V2.",
default: false,
category: "beta",
},
};
26 changes: 26 additions & 0 deletions src/hooks/useNaturalLanguageComponentSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ import { useMutation } from "@tanstack/react-query";

import { useComponentSearchSettings } from "@/hooks/useComponentSearchSettings";
import {
type ComponentDescriptionResult,
generateComponentAiDescription,
type RerankCandidate,
rerankComponentsByNaturalLanguage,
type RerankResult,
} from "@/services/naturalLanguageComponentSearchService";
import type { ComponentReference } from "@/utils/componentSpec";

interface RerankVariables {
query: string;
candidates: RerankCandidate[];
}

interface DescriptionVariables {
reference: ComponentReference;
}

/**
* Trigger an LLM rerank of pre-filtered candidates. Modeled as a mutation
* rather than a query because rerank is **explicitly initiated** by the user
Expand All @@ -36,3 +43,22 @@ export function useNaturalLanguageComponentRerank() {

return { ...mutation, isConfigured };
}

export function useComponentAiDescription() {
const { config, isConfigured } = useComponentSearchSettings();

const mutation = useMutation<
ComponentDescriptionResult,
Error,
DescriptionVariables
>({
mutationFn: ({ reference }) =>
generateComponentAiDescription(reference, {
model: config.model,
apiBase: config.apiBase,
apiKey: config.apiKey,
}),
});

return { ...mutation, isConfigured };
}
148 changes: 148 additions & 0 deletions src/routes/Dashboard/DashboardComponentsV2SourceFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Paragraph, Text } from "@/components/ui/typography";
import { cn } from "@/lib/utils";
import type {
ComponentSource,
IndexEntry,
} from "@/services/componentSearchIndex";
import { tracking } from "@/utils/tracking";

const SOURCE_ICON_TONE_BY_KIND: Record<ComponentSource["kind"], string> = {
standard: "text-blue-500",
published: "text-emerald-500",
registered: "text-violet-500",
user: "text-amber-500",
};

const SOURCE_FILTER_LABEL_BY_KIND: Record<ComponentSource["kind"], string> = {
standard: "Standard",
published: "Published",
registered: "Registered libraries",
user: "User generated",
};

export interface SourceFilterOption {
source: ComponentSource;
count: number;
}

function sourceFilterKey(source: ComponentSource): string {
return source.kind;
}

/**
* Filter state is keyed by source kind so multiple registered libraries behave
* as one source category instead of several per-library toggles.
*/
export function createSourceFilterOptions(
index: IndexEntry[],
): SourceFilterOption[] {
const optionsByKey = new Map<string, SourceFilterOption>();

for (const entry of index) {
const key = sourceFilterKey(entry.source);
const option = optionsByKey.get(key);
if (option) {
option.count += 1;
} else {
optionsByKey.set(key, {
source: {
kind: entry.source.kind,
id: entry.source.kind,
label: SOURCE_FILTER_LABEL_BY_KIND[entry.source.kind],
},
count: 1,
});
}
}

return Array.from(optionsByKey.values());
}

/** Apply source-type filter state to indexed component results. */
export function filterIndexByDisabledSourceKeys(
index: IndexEntry[],
disabledSourceKeys: string[],
): IndexEntry[] {
const disabled = new Set(disabledSourceKeys);
return index.filter((entry) => !disabled.has(sourceFilterKey(entry.source)));
}

interface SourceFilterBarProps {
options: SourceFilterOption[];
disabledSourceKeys: string[];
onToggle: (sourceKey: string) => void;
onEnableAll: () => void;
}

export const SourceFilterBar = ({
options,
disabledSourceKeys,
onToggle,
onEnableAll,
}: SourceFilterBarProps) => {
const disabled = new Set(disabledSourceKeys);
const activeCount = options.filter(
(option) => !disabled.has(sourceFilterKey(option.source)),
).length;

if (options.length <= 1 && activeCount === options.length) return null;

return (
<BlockStack gap="2">
<InlineStack gap="2" blockAlign="center" wrap="wrap">
<Text size="xs" tone="subdued">
Sources
</Text>
{options.map(({ source, count }) => {
const key = sourceFilterKey(source);
const active = !disabled.has(key);
return (
<Button
key={key}
type="button"
size="xs"
variant={active ? "secondary" : "outline"}
aria-pressed={active}
aria-label={`${source.label} source (${count} component${count === 1 ? "" : "s"})`}
onClick={() => onToggle(key)}
className={cn(!active && "opacity-60")}
{...tracking("component_library.source_filter", {
source_kind: source.kind,
enabled_after_click: !active,
})}
>
<Icon
name="Package"
size="sm"
className={SOURCE_ICON_TONE_BY_KIND[source.kind]}
/>
{source.label}
<Text as="span" size="xs" tone="subdued">
{count}
</Text>
</Button>
);
})}
{activeCount < options.length && (
<Button
type="button"
size="xs"
variant="ghost"
onClick={onEnableAll}
{...tracking("component_library.source_filter.show_all_button")}
>
Show all
</Button>
)}
</InlineStack>
{activeCount === 0 && (
<Paragraph size="xs" tone="subdued">
No sources selected. Turn on at least one source to show components.
</Paragraph>
)}
</BlockStack>
);
};
Loading
Loading