Studio: restructure Knowledge bases tab layout; hint under header, compact when empty
This commit is contained in:
parent
b17b3ad727
commit
71b5214ca7
3 changed files with 107 additions and 89 deletions
|
|
@ -25,6 +25,11 @@ vi.mock("@/features/rag/components/kb-list", () => ({
|
|||
KBList: () => React.createElement("div", { "data-testid": "kb-list" }),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/rag/components/kb-create-dialog", () => ({
|
||||
KBCreateDialog: () =>
|
||||
React.createElement("div", { "data-testid": "kb-create-dialog" }),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/rag/components/kb-detail-panel", () => ({
|
||||
KBDetailPanel: () =>
|
||||
React.createElement("div", { "data-testid": "kb-detail-panel" }),
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Add01Icon, Delete02Icon } from "@hugeicons/core-free-icons";
|
||||
import { Delete02Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { useState } from "react";
|
||||
import type { KnowledgeBase } from "../api/rag-api";
|
||||
import { useKnowledgeBases } from "../hooks/use-knowledge-bases";
|
||||
import { KBCreateDialog } from "./kb-create-dialog";
|
||||
|
||||
export function KBList({
|
||||
selectedId,
|
||||
|
|
@ -19,25 +17,14 @@ export function KBList({
|
|||
onSelect: (kb: KnowledgeBase | null) => void;
|
||||
}) {
|
||||
const { knowledgeBases, loading, error, deleteKB } = useKnowledgeBases();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">Knowledge bases</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Create knowledge base"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={Add01Icon} size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
{error ? <div className="text-xs text-destructive">{error}</div> : null}
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
{/* Capped, content-sized list: stays short when empty (no oddly-tall
|
||||
box) and scrolls internally once there are many bases. */}
|
||||
<ScrollArea className="max-h-[320px]">
|
||||
<div className="flex flex-col gap-1 pr-2">
|
||||
{knowledgeBases.length === 0 && !loading ? (
|
||||
<div className="rounded-md border border-dashed border-border/60 px-3 py-6 text-center text-xs text-muted-foreground">
|
||||
|
|
@ -108,12 +95,6 @@ export function KBList({
|
|||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<KBCreateDialog
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
onCreated={(kb) => onSelect(kb)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import type { KnowledgeBase } from "@/features/rag/api/rag-api";
|
||||
import { KBCreateDialog } from "@/features/rag/components/kb-create-dialog";
|
||||
import { KBDetailPanel } from "@/features/rag/components/kb-detail-panel";
|
||||
import { KBList } from "@/features/rag/components/kb-list";
|
||||
import { PreviewPanel } from "@/features/rag/components/preview-panel";
|
||||
|
|
@ -11,10 +13,17 @@ import { ThreadIndexList } from "@/features/rag/components/thread-index-list";
|
|||
import { useResizablePanelWidth } from "@/features/rag/hooks/use-resizable-width";
|
||||
import { usePreviewStore } from "@/features/rag/stores/preview-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Add01Icon } from "@hugeicons/core-free-icons";
|
||||
import { HugeiconsIcon } from "@hugeicons/react";
|
||||
import { type CSSProperties, useState } from "react";
|
||||
|
||||
// Height of the master-detail workspace shown once a knowledge base is
|
||||
// selected. Only rendered then, so the section stays compact when empty.
|
||||
const KB_WORKSPACE_HEIGHT = "h-[360px]";
|
||||
|
||||
export function KnowledgeBasesTab() {
|
||||
const [selected, setSelected] = useState<KnowledgeBase | null>(null);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const previewTarget = usePreviewStore((s) => s.target);
|
||||
const previewStatus = usePreviewStore((s) => s.status);
|
||||
const previewActive =
|
||||
|
|
@ -46,76 +55,99 @@ export function KnowledgeBasesTab() {
|
|||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
{/* Fixed-height workspace so the KB list / detail / preview have a
|
||||
bounded area to scroll within; the rest of the tab flows and the
|
||||
settings dialog body scrolls. A flex-1 fill here collapsed under the
|
||||
tall thread-index + defaults sections below, overlapping them. */}
|
||||
<div className="flex h-[360px] min-h-0 gap-4">
|
||||
<div className="w-[220px] shrink-0">
|
||||
<KBList selectedId={selected?.id ?? null} onSelect={setSelected} />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">Knowledge bases</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label="Create knowledge base"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<HugeiconsIcon icon={Add01Icon} size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<Separator orientation="vertical" />
|
||||
<div className="min-w-0 flex-1">
|
||||
{selected ? (
|
||||
<KBDetailPanel kb={selected} />
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
Select a knowledge base, or create a new one to get started.
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Select a knowledge base to manage its documents, or create a new one
|
||||
to get started.
|
||||
</p>
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-0 gap-4",
|
||||
(selected || previewActive) && KB_WORKSPACE_HEIGHT,
|
||||
)}
|
||||
</div>
|
||||
{previewActive ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Resize preview panel — drag, or use arrow keys"
|
||||
aria-orientation="vertical"
|
||||
onPointerDown={startPreviewResize}
|
||||
onDoubleClick={resetPreviewWidth}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
adjustPreviewWidth(24);
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
adjustPreviewWidth(-24);
|
||||
} else if (event.key === "Home") {
|
||||
event.preventDefault();
|
||||
resetPreviewWidth();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"group relative hidden w-1.5 cursor-col-resize touch-none select-none border-0 bg-transparent p-0 outline-none lg:block",
|
||||
"before:absolute before:inset-y-0 before:left-1/2 before:-translate-x-1/2 before:w-px before:bg-border/70 before:transition-colors",
|
||||
"hover:before:bg-primary/40 focus-visible:before:bg-primary/60",
|
||||
previewResizing && "before:bg-primary/60",
|
||||
)}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div className="w-[220px] shrink-0">
|
||||
<KBList selectedId={selected?.id ?? null} onSelect={setSelected} />
|
||||
</div>
|
||||
{selected ? (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-auto" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<KBDetailPanel kb={selected} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
{previewActive ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Resize preview panel — drag, or use arrow keys"
|
||||
aria-orientation="vertical"
|
||||
onPointerDown={startPreviewResize}
|
||||
onDoubleClick={resetPreviewWidth}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
adjustPreviewWidth(24);
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
adjustPreviewWidth(-24);
|
||||
} else if (event.key === "Home") {
|
||||
event.preventDefault();
|
||||
resetPreviewWidth();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"absolute left-1/2 top-1/2 h-10 w-[3px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-border opacity-0 transition-opacity",
|
||||
"group-hover:opacity-100 group-focus-visible:opacity-100",
|
||||
previewResizing && "opacity-100",
|
||||
"group relative hidden w-1.5 cursor-col-resize touch-none select-none border-0 bg-transparent p-0 outline-none lg:block",
|
||||
"before:absolute before:inset-y-0 before:left-1/2 before:-translate-x-1/2 before:w-px before:bg-border/70 before:transition-colors",
|
||||
"hover:before:bg-primary/40 focus-visible:before:bg-primary/60",
|
||||
previewResizing && "before:bg-primary/60",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
"w-0 shrink-0 overflow-hidden max-lg:hidden lg:w-[var(--preview-w)]",
|
||||
!previewResizing && "transition-[width] duration-200 ease-out",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--preview-w": `${previewWidth}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<PreviewPanel open={previewActive} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"absolute left-1/2 top-1/2 h-10 w-[3px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-border opacity-0 transition-opacity",
|
||||
"group-hover:opacity-100 group-focus-visible:opacity-100",
|
||||
previewResizing && "opacity-100",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={cn(
|
||||
"w-0 shrink-0 overflow-hidden max-lg:hidden lg:w-[var(--preview-w)]",
|
||||
!previewResizing && "transition-[width] duration-200 ease-out",
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--preview-w": `${previewWidth}px`,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<PreviewPanel open={previewActive} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
<KBCreateDialog
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
onCreated={(kb) => setSelected(kb)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
<ThreadIndexList />
|
||||
<Separator />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue