Studio chat: declutter the plus menu, session-only pre-selectable RAG (#6140)
This commit is contained in:
parent
3cd0b9b18e
commit
e4b5bec248
6 changed files with 190 additions and 183 deletions
|
|
@ -64,7 +64,7 @@ import { NewProjectDialog } from "@/features/chat/components/new-project-dialog"
|
|||
import { parseExternalModelId } from "@/features/chat/external-providers";
|
||||
import { McpComposerButton } from "@/features/chat/mcp-composer-button";
|
||||
import { getExternalReasoningCapabilities } from "@/features/chat/provider-capabilities";
|
||||
import { useRagToolAvailable } from "@/features/chat/hooks/use-rag-tool-available";
|
||||
import { useRagToolDisabled } from "@/features/chat/hooks/use-rag-tool-disabled";
|
||||
import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store";
|
||||
import { useExternalProvidersStore } from "@/features/chat/stores/external-providers-store";
|
||||
import { deleteThreadMessage } from "@/features/chat/utils/delete-thread-message";
|
||||
|
|
@ -876,12 +876,11 @@ const Composer: FC<{
|
|||
const artifactsEnabled = useChatRuntimeStore((s) => s.artifactsEnabled);
|
||||
const mcpEnabledForChat = useChatRuntimeStore((s) => s.mcpEnabledForChat);
|
||||
const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled);
|
||||
const ragToolAvailable = useRagToolAvailable();
|
||||
// More than 4 pills: collapse to icons only. Search and Code always show;
|
||||
// Images, Canvas and MCP are conditional.
|
||||
// Images, RAG, Canvas and MCP are conditional.
|
||||
const pillsCompact =
|
||||
2 +
|
||||
(ragEnabled && ragToolAvailable ? 1 : 0) +
|
||||
(ragEnabled ? 1 : 0) +
|
||||
(supportsBuiltinImageGeneration ? 1 : 0) +
|
||||
(artifactsEnabled ? 1 : 0) +
|
||||
(mcpEnabledForChat ? 1 : 0) >
|
||||
|
|
@ -2017,8 +2016,8 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
|
|||
);
|
||||
const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled);
|
||||
const setRagEnabled = useChatRuntimeStore((s) => s.setRagEnabled);
|
||||
// Shared gate so the menu row agrees with the RAG pill and Add Files bar.
|
||||
const ragAvailable = useRagToolAvailable();
|
||||
// Shared gate so the menu row agrees with the RAG pill.
|
||||
const ragDisabled = useRagToolDisabled();
|
||||
// Capability gating mirrors the visible pills so menu and pills agree on
|
||||
// what a loaded model supports (a tool the backend drops must not look on).
|
||||
const modelLoaded = useChatRuntimeStore(
|
||||
|
|
@ -2180,10 +2179,11 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
|
|||
}
|
||||
onSelect={() => setCodeToolsEnabled(!codeToolsEnabled)}
|
||||
>
|
||||
{/* Scale, not width: an oversized box pushed the label out of line. */}
|
||||
<HugeiconsIcon
|
||||
icon={CodeIcon}
|
||||
strokeWidth={2}
|
||||
className="size-[1.175rem]!"
|
||||
className="scale-[1.12]"
|
||||
/>
|
||||
Code
|
||||
{codeToolsEnabled && !codeDisabled ? (
|
||||
|
|
@ -2217,15 +2217,15 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
|
|||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
disabled={!ragAvailable}
|
||||
disabled={ragDisabled}
|
||||
className={
|
||||
ragEnabled && ragAvailable ? "text-primary font-medium" : undefined
|
||||
ragEnabled && !ragDisabled ? "text-primary font-medium" : undefined
|
||||
}
|
||||
onSelect={() => setRagEnabled(!ragEnabled)}
|
||||
>
|
||||
<HugeiconsIcon icon={FileDatabaseIcon} strokeWidth={2} />
|
||||
RAG
|
||||
{ragEnabled && ragAvailable ? (
|
||||
Chat with Files
|
||||
{ragEnabled && !ragDisabled ? (
|
||||
<HugeiconsIcon
|
||||
icon={Tick02Icon}
|
||||
strokeWidth={2}
|
||||
|
|
@ -2252,80 +2252,82 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({
|
|||
/>
|
||||
) : null}
|
||||
</DropdownMenuItem>
|
||||
{/* Top-level so it stays one click away (not buried in More). */}
|
||||
<DropdownMenuItem onSelect={() => startCompare()}>
|
||||
<Columns2Icon />
|
||||
Compare chat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<HugeiconsIcon icon={Bookmark02Icon} strokeWidth={2} />
|
||||
Saved prompts
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="unsloth-plus-menu w-[176px]">
|
||||
{recentPrompts.map((p) => (
|
||||
<DropdownMenuItem
|
||||
key={p.id}
|
||||
onSelect={() => aui.composer().setText(p.text)}
|
||||
>
|
||||
<span className="truncate">{p.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{recentPrompts.length > 0 ? <DropdownMenuSeparator /> : null}
|
||||
<DropdownMenuItem onSelect={() => setPromptStorageOpen(true)}>
|
||||
All saved prompts…
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
{/* Top-level: a third-level submenu collision-flips at narrow widths
|
||||
and is awkward to reach. */}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger disabled={!activeThreadId || messageCount === 0}>
|
||||
<HugeiconsIcon icon={Download01Icon} strokeWidth={2} />
|
||||
Export chat
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
collisionPadding={16}
|
||||
className="unsloth-plus-menu w-[176px]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
if (!activeThreadId) return;
|
||||
exportConversationRawJsonl(activeThreadId).catch(() =>
|
||||
toast.error("Export failed."),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Raw JSONL
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
if (!activeThreadId) return;
|
||||
exportConversationCsv(activeThreadId).catch(() =>
|
||||
toast.error("Export failed."),
|
||||
);
|
||||
}}
|
||||
>
|
||||
CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
if (!activeThreadId) return;
|
||||
exportConversationShareGPT(activeThreadId).catch(() =>
|
||||
toast.error("Export failed."),
|
||||
);
|
||||
}}
|
||||
>
|
||||
ShareGPT JSONL
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
More
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-[200px]">
|
||||
<DropdownMenuItem onSelect={() => startCompare()}>
|
||||
<Columns2Icon />
|
||||
Compare chat
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<HugeiconsIcon icon={Bookmark02Icon} strokeWidth={2} />
|
||||
Saved prompts
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
collisionPadding={16}
|
||||
className="unsloth-plus-menu w-[176px]"
|
||||
>
|
||||
{recentPrompts.map((p) => (
|
||||
<DropdownMenuItem
|
||||
key={p.id}
|
||||
onSelect={() => aui.composer().setText(p.text)}
|
||||
>
|
||||
<span className="truncate">{p.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{recentPrompts.length > 0 ? <DropdownMenuSeparator /> : null}
|
||||
<DropdownMenuItem onSelect={() => setPromptStorageOpen(true)}>
|
||||
All saved prompts…
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger
|
||||
disabled={!activeThreadId || messageCount === 0}
|
||||
>
|
||||
<HugeiconsIcon icon={Download01Icon} strokeWidth={2} />
|
||||
Export chat
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
collisionPadding={16}
|
||||
className="unsloth-plus-menu w-[176px]"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
if (!activeThreadId) return;
|
||||
exportConversationRawJsonl(activeThreadId).catch(() =>
|
||||
toast.error("Export failed."),
|
||||
);
|
||||
}}
|
||||
>
|
||||
Raw JSONL
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
if (!activeThreadId) return;
|
||||
exportConversationCsv(activeThreadId).catch(() =>
|
||||
toast.error("Export failed."),
|
||||
);
|
||||
}}
|
||||
>
|
||||
CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
if (!activeThreadId) return;
|
||||
exportConversationShareGPT(activeThreadId).catch(() =>
|
||||
toast.error("Export failed."),
|
||||
);
|
||||
}}
|
||||
>
|
||||
ShareGPT JSONL
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem
|
||||
className={
|
||||
artifactsEnabled ? "text-primary font-medium" : undefined
|
||||
|
|
|
|||
|
|
@ -4,15 +4,16 @@
|
|||
import { parseExternalModelId } from "../external-providers";
|
||||
import { useChatRuntimeStore } from "../stores/chat-runtime-store";
|
||||
|
||||
// Single source of truth for the RAG pill's disabled gate and the Add Files bar's
|
||||
// visibility so the bar never shows while the pill is inert.
|
||||
export function useRagToolAvailable(): boolean {
|
||||
// Pre-select gate for the RAG toggle, mirroring Web search/Code/MCP: armable
|
||||
// with no model; disabled only when a loaded model can't run
|
||||
// search_knowledge_base. The send path checks supportsTools independently.
|
||||
export function useRagToolDisabled(): boolean {
|
||||
const modelLoaded = useChatRuntimeStore(
|
||||
(s) => !!s.params.checkpoint && !s.modelLoading,
|
||||
);
|
||||
const checkpoint = useChatRuntimeStore((s) => s.params.checkpoint);
|
||||
const supportsTools = useChatRuntimeStore((s) => s.supportsTools);
|
||||
return (
|
||||
modelLoaded && parseExternalModelId(checkpoint) === null && supportsTools
|
||||
modelLoaded && (parseExternalModelId(checkpoint) !== null || !supportsTools)
|
||||
);
|
||||
}
|
||||
|
|
@ -656,8 +656,10 @@ export function SharedComposer({
|
|||
// Fetch pill: Anthropic-only (web_fetch_20250910 / web_fetch_20260209).
|
||||
const webFetchDisabled = !modelLoaded || !supportsBuiltinWebFetch;
|
||||
const showWebFetchPill = supportsBuiltinWebFetch;
|
||||
// Docs (RAG) pill is local-only: search_knowledge_base needs the local runtime.
|
||||
const ragDisabled = !modelLoaded || isExternalModel || !supportsTools;
|
||||
// Docs (RAG) is local-only: search_knowledge_base needs the local runtime.
|
||||
// Disable only when a loaded model can't run it; with no model the toggle
|
||||
// can still be pre-selected, matching Web search/Code/MCP.
|
||||
const ragDisabled = modelLoaded && (isExternalModel || !supportsTools);
|
||||
const showRagPill = !isExternalModel;
|
||||
// Above 4 pills, collapse to icons only to cut clutter. Compare, Search and
|
||||
// Code always show; the rest are conditional.
|
||||
|
|
@ -1323,10 +1325,12 @@ export function SharedComposer({
|
|||
}
|
||||
onSelect={() => setCodeToolsEnabled(!codeToolsEnabled)}
|
||||
>
|
||||
{/* Scale, not width: an oversized box pushed the label out of
|
||||
line. */}
|
||||
<HugeiconsIcon
|
||||
icon={CodeIcon}
|
||||
strokeWidth={2}
|
||||
className="size-[1.175rem]!"
|
||||
className="scale-[1.12]"
|
||||
/>
|
||||
Code
|
||||
{codeToolsEnabled && !codeDisabled ? (
|
||||
|
|
@ -1369,7 +1373,7 @@ export function SharedComposer({
|
|||
onSelect={() => setRagEnabled(!ragEnabled)}
|
||||
>
|
||||
<HugeiconsIcon icon={FileDatabaseIcon} strokeWidth={2} />
|
||||
RAG
|
||||
Chat with Files
|
||||
{ragEnabled && !ragDisabled ? (
|
||||
<HugeiconsIcon
|
||||
icon={Tick02Icon}
|
||||
|
|
@ -1396,88 +1400,95 @@ export function SharedComposer({
|
|||
) : null}
|
||||
</DropdownMenuItem>
|
||||
{/* RAG hidden temporarily */}
|
||||
{/* Always active: this menu only renders in compare mode. Ticked
|
||||
like Web search/Code; click toggles it off. */}
|
||||
<DropdownMenuItem
|
||||
className="text-primary font-medium"
|
||||
onSelect={handleExitCompare}
|
||||
>
|
||||
<Columns2Icon />
|
||||
Compare chat
|
||||
<HugeiconsIcon
|
||||
icon={Tick02Icon}
|
||||
strokeWidth={2}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<HugeiconsIcon icon={Bookmark02Icon} strokeWidth={2} />
|
||||
Saved prompts
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="unsloth-plus-menu w-[176px]">
|
||||
{recentPrompts.map((p) => (
|
||||
<DropdownMenuItem
|
||||
key={p.id}
|
||||
onSelect={() => {
|
||||
setText(p.text);
|
||||
requestAnimationFrame(() =>
|
||||
textareaRef.current?.focus(),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{p.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{recentPrompts.length > 0 ? <DropdownMenuSeparator /> : null}
|
||||
<DropdownMenuItem onSelect={() => setPromptStorageOpen(true)}>
|
||||
All saved prompts…
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
{/* Top-level: a third-level submenu collision-flips at narrow
|
||||
widths and is awkward to reach. */}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger disabled={exportThreadIds.length === 0}>
|
||||
<HugeiconsIcon icon={Download01Icon} strokeWidth={2} />
|
||||
Export chat
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
collisionPadding={16}
|
||||
className="unsloth-plus-menu w-[176px]"
|
||||
>
|
||||
{[
|
||||
{ label: "Raw JSONL", fn: exportConversationRawJsonl },
|
||||
{ label: "CSV", fn: exportConversationCsv },
|
||||
{
|
||||
label: "ShareGPT JSONL",
|
||||
fn: exportConversationShareGPT,
|
||||
},
|
||||
].map(({ label, fn }) => (
|
||||
<DropdownMenuItem
|
||||
key={label}
|
||||
disabled={exportThreadIds.length === 0}
|
||||
onSelect={() => {
|
||||
if (!exportThreadIds.length) {
|
||||
toast.error("No conversation to export yet.");
|
||||
return;
|
||||
}
|
||||
Promise.all(exportThreadIds.map((id) => fn(id))).catch(
|
||||
() => toast.error("Export failed."),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
More
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="unsloth-plus-menu w-[200px]">
|
||||
{/* Always active: this menu only renders in compare mode. Ticked
|
||||
like Web search/Code; click toggles it off. */}
|
||||
<DropdownMenuItem
|
||||
className="text-primary font-medium"
|
||||
onSelect={handleExitCompare}
|
||||
>
|
||||
<Columns2Icon />
|
||||
Compare chat
|
||||
<HugeiconsIcon
|
||||
icon={Tick02Icon}
|
||||
strokeWidth={2}
|
||||
className="ml-auto"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<HugeiconsIcon icon={Bookmark02Icon} strokeWidth={2} />
|
||||
Saved prompts
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
collisionPadding={16}
|
||||
className="unsloth-plus-menu w-[176px]"
|
||||
>
|
||||
{recentPrompts.map((p) => (
|
||||
<DropdownMenuItem
|
||||
key={p.id}
|
||||
onSelect={() => {
|
||||
setText(p.text);
|
||||
requestAnimationFrame(() =>
|
||||
textareaRef.current?.focus(),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="truncate">{p.name}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
{recentPrompts.length > 0 ? (
|
||||
<DropdownMenuSeparator />
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
onSelect={() => setPromptStorageOpen(true)}
|
||||
>
|
||||
All saved prompts…
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger
|
||||
disabled={exportThreadIds.length === 0}
|
||||
>
|
||||
<HugeiconsIcon icon={Download01Icon} strokeWidth={2} />
|
||||
Export chat
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent
|
||||
collisionPadding={16}
|
||||
className="unsloth-plus-menu w-[176px]"
|
||||
>
|
||||
{[
|
||||
{ label: "Raw JSONL", fn: exportConversationRawJsonl },
|
||||
{ label: "CSV", fn: exportConversationCsv },
|
||||
{
|
||||
label: "ShareGPT JSONL",
|
||||
fn: exportConversationShareGPT,
|
||||
},
|
||||
].map(({ label, fn }) => (
|
||||
<DropdownMenuItem
|
||||
key={label}
|
||||
disabled={exportThreadIds.length === 0}
|
||||
onSelect={() => {
|
||||
if (!exportThreadIds.length) {
|
||||
toast.error("No conversation to export yet.");
|
||||
return;
|
||||
}
|
||||
Promise.all(
|
||||
exportThreadIds.map((id) => fn(id)),
|
||||
).catch(() => toast.error("Export failed."));
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuItem
|
||||
className={
|
||||
artifactsEnabled ? "text-primary font-medium" : undefined
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ export const CHAT_ALLOW_ARTIFACT_NETWORK_ACCESS_KEY =
|
|||
export const CHAT_MCP_ENABLED_KEY = "unsloth_chat_mcp_enabled";
|
||||
export const CHAT_WEB_FETCH_TOOLS_ENABLED_KEY =
|
||||
"unsloth_chat_web_fetch_tools_enabled";
|
||||
export const CHAT_RAG_ENABLED_KEY = "unsloth_chat_rag_enabled";
|
||||
export const CHAT_RAG_SOURCE_KEY = "unsloth_chat_rag_source";
|
||||
export const CHAT_RAG_MODE_KEY = "unsloth_chat_rag_mode";
|
||||
export const CHAT_RAG_TOP_K_KEY = "unsloth_chat_rag_top_k";
|
||||
|
|
@ -480,7 +479,7 @@ type ChatRuntimeStore = {
|
|||
setAllowArtifactNetworkAccess: (enabled: boolean) => void;
|
||||
setMcpEnabledForChat: (enabled: boolean) => void;
|
||||
setWebFetchToolsEnabled: (enabled: boolean) => void;
|
||||
setRagEnabled: (enabled: boolean, options?: { persist?: boolean }) => void;
|
||||
setRagEnabled: (enabled: boolean) => void;
|
||||
setRagSource: (source: RagSource) => void;
|
||||
setRagMode: (mode: RagMode) => void;
|
||||
setRagTopK: (topK: number) => void;
|
||||
|
|
@ -746,7 +745,8 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
|
|||
),
|
||||
mcpEnabledForChat: loadBool(CHAT_MCP_ENABLED_KEY, false),
|
||||
webFetchToolsEnabled: loadBool(CHAT_WEB_FETCH_TOOLS_ENABLED_KEY, false),
|
||||
ragEnabled: loadBool(CHAT_RAG_ENABLED_KEY, false),
|
||||
// RAG is opt-in per session: always starts off, never restored from storage.
|
||||
ragEnabled: false,
|
||||
ragSource: loadRagSource(),
|
||||
ragMode: loadRagMode(),
|
||||
ragTopK: loadRagTopK(),
|
||||
|
|
@ -1072,13 +1072,7 @@ export const useChatRuntimeStore = create<ChatRuntimeStore>((set, get) => ({
|
|||
saveBool(CHAT_WEB_FETCH_TOOLS_ENABLED_KEY, webFetchToolsEnabled);
|
||||
return { webFetchToolsEnabled };
|
||||
}),
|
||||
setRagEnabled: (ragEnabled, options) =>
|
||||
set(() => {
|
||||
if (options?.persist !== false) {
|
||||
saveBool(CHAT_RAG_ENABLED_KEY, ragEnabled);
|
||||
}
|
||||
return { ragEnabled };
|
||||
}),
|
||||
setRagEnabled: (ragEnabled) => set(() => ({ ragEnabled })),
|
||||
setRagSource: (ragSource) =>
|
||||
set(() => {
|
||||
saveRagSource(ragSource);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useRagToolAvailable } from "@/features/chat/hooks/use-rag-tool-available";
|
||||
import { useRagToolDisabled } from "@/features/chat/hooks/use-rag-tool-disabled";
|
||||
import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store";
|
||||
|
||||
import { listKnowledgeBases } from "../api/rag-api";
|
||||
|
|
@ -38,8 +38,8 @@ const ArrowDownStandardIcon: FC<{ className?: string }> = ({ className }) => (
|
|||
</svg>
|
||||
);
|
||||
|
||||
// Picks the retrieval source. Only rendered when retrieval is on and the loaded
|
||||
// model can run search_knowledge_base.
|
||||
// Picks the retrieval source. Shown whenever retrieval is on; dims but stays
|
||||
// interactive (so it can be turned off) while the loaded model can't run it.
|
||||
export function KnowledgeBaseComposerButton({
|
||||
side = "bottom",
|
||||
}: {
|
||||
|
|
@ -47,7 +47,7 @@ export function KnowledgeBaseComposerButton({
|
|||
} = {}) {
|
||||
const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled);
|
||||
const setRagEnabled = useChatRuntimeStore((s) => s.setRagEnabled);
|
||||
const ragAvailable = useRagToolAvailable();
|
||||
const ragDisabled = useRagToolDisabled();
|
||||
const ragSource = useChatRuntimeStore((s) => s.ragSource);
|
||||
const setRagSource = useChatRuntimeStore((s) => s.setRagSource);
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ export function KnowledgeBaseComposerButton({
|
|||
}
|
||||
}, [kbs, kbsLoaded, ragSource, setRagSource]);
|
||||
|
||||
if (!ragEnabled || !ragAvailable) return null;
|
||||
if (!ragEnabled) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -100,7 +100,7 @@ export function KnowledgeBaseComposerButton({
|
|||
<button
|
||||
type="button"
|
||||
className="composer-pill-btn"
|
||||
data-active="true"
|
||||
data-active={ragDisabled ? "false" : "true"}
|
||||
aria-label="Retrieval source"
|
||||
>
|
||||
{/* Icon doubles as an off switch: hover swaps to an X; clicking it
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { AttachmentIcon, FileDatabaseIcon } from "@hugeicons/core-free-icons";
|
|||
import { useAui } from "@assistant-ui/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store";
|
||||
import { useRagToolAvailable } from "@/features/chat/hooks/use-rag-tool-available";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { listKnowledgeBases, listThreadDocuments } from "../api/rag-api";
|
||||
import { RAG_UPLOAD_ACCEPT } from "../types/rag";
|
||||
|
|
@ -55,7 +54,6 @@ export function ThreadDocumentsBar({
|
|||
onIndexingChange?: (active: boolean) => void;
|
||||
}) {
|
||||
const ragEnabled = useChatRuntimeStore((s) => s.ragEnabled);
|
||||
const ragAvailable = useRagToolAvailable();
|
||||
const ragSource = useChatRuntimeStore((s) => s.ragSource);
|
||||
const aui = useAui();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -137,8 +135,9 @@ export function ThreadDocumentsBar({
|
|||
fileInputRef.current?.click();
|
||||
}, []);
|
||||
|
||||
// Only when the RAG pill is on: enabled AND a tool-capable model loaded.
|
||||
if (!ragEnabled || !ragAvailable) return null;
|
||||
// Shown whenever the RAG pill is on: ingestion only needs the embedder, so
|
||||
// files can index before a chat model loads.
|
||||
if (!ragEnabled) return null;
|
||||
// A KB source uploads via the KB dialog, not here; show which KB is active.
|
||||
if (ragSource.type === "kb") {
|
||||
return <KnowledgeBaseSourceChip kbId={ragSource.kbId} />;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue