mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
further improvements
This commit is contained in:
parent
c1467bcd82
commit
154c95f4ee
17 changed files with 285 additions and 181 deletions
|
|
@ -56,6 +56,7 @@ export default async function Page(props: PageProps) {
|
||||||
<>
|
<>
|
||||||
<TopBar
|
<TopBar
|
||||||
domain={params.domain}
|
domain={params.domain}
|
||||||
|
homePath={`/${params.domain}/chat`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row gap-2 items-center">
|
<div className="flex flex-row gap-2 items-center">
|
||||||
<span className="text-muted mx-2 select-none">/</span>
|
<span className="text-muted mx-2 select-none">/</span>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useLocalStorage } from "usehooks-ts";
|
import { useLocalStorage } from "usehooks-ts";
|
||||||
import { SearchModeSelector } from "../../components/searchModeSelector";
|
import { SearchModeSelector } from "../../components/searchModeSelector";
|
||||||
|
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
|
||||||
|
|
||||||
interface LandingPageChatBox {
|
interface LandingPageChatBox {
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
|
|
@ -24,9 +25,13 @@ export const LandingPageChatBox = ({
|
||||||
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
||||||
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false });
|
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false });
|
||||||
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
||||||
|
const isChatBoxDisabled = languageModels.length === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center w-full">
|
<div className="w-full max-w-[800px] mt-4">
|
||||||
<div className="mt-4 w-full border rounded-md shadow-sm max-w-[800px]">
|
|
||||||
|
|
||||||
|
<div className="border rounded-md w-full shadow-sm">
|
||||||
<ChatBox
|
<ChatBox
|
||||||
onSubmit={(children) => {
|
onSubmit={(children) => {
|
||||||
createNewChatThread(children, selectedSearchScopes);
|
createNewChatThread(children, selectedSearchScopes);
|
||||||
|
|
@ -37,6 +42,7 @@ export const LandingPageChatBox = ({
|
||||||
selectedSearchScopes={selectedSearchScopes}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
searchContexts={searchContexts}
|
searchContexts={searchContexts}
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||||
|
isDisabled={isChatBoxDisabled}
|
||||||
/>
|
/>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|
@ -57,6 +63,10 @@ export const LandingPageChatBox = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isChatBoxDisabled && (
|
||||||
|
<NotConfiguredErrorBanner className="mt-4" />
|
||||||
|
)}
|
||||||
</div >
|
</div >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -8,18 +8,20 @@ import { Separator } from "@/components/ui/separator";
|
||||||
interface TopBarProps {
|
interface TopBarProps {
|
||||||
domain: string;
|
domain: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
|
homePath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopBar = ({
|
export const TopBar = ({
|
||||||
domain,
|
domain,
|
||||||
children,
|
children,
|
||||||
|
homePath = `/${domain}`,
|
||||||
}: TopBarProps) => {
|
}: TopBarProps) => {
|
||||||
return (
|
return (
|
||||||
<div className='sticky top-0 left-0 right-0 z-10'>
|
<div className='sticky top-0 left-0 right-0 z-10'>
|
||||||
<div className="flex flex-row justify-between items-center py-1.5 px-3 gap-4 bg-background">
|
<div className="flex flex-row justify-between items-center py-1.5 px-3 gap-4 bg-background">
|
||||||
<div className="grow flex flex-row gap-4 items-center">
|
<div className="grow flex flex-row gap-4 items-center">
|
||||||
<Link
|
<Link
|
||||||
href={`/${domain}`}
|
href={homePath}
|
||||||
className="shrink-0 cursor-pointer"
|
className="shrink-0 cursor-pointer"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,8 @@ export default async function Layout(
|
||||||
props: LayoutProps
|
props: LayoutProps
|
||||||
) {
|
) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
|
const { domain } = params;
|
||||||
const {
|
const { children } = props;
|
||||||
domain
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
const {
|
|
||||||
children
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { sew, withAuth, withOrgMembership } from "@/actions";
|
import { sew, withAuth, withOrgMembership } from "@/actions";
|
||||||
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, updateChatMessages } from "@/features/chat/actions";
|
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, updateChatMessages } from "@/features/chat/actions";
|
||||||
import { createAgentStream } from "@/features/chat/agent";
|
import { createAgentStream } from "@/features/chat/agent";
|
||||||
import { additionalChatRequestParamsSchema, SBChatMessage, SearchScope } from "@/features/chat/types";
|
import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types";
|
||||||
import { getAnswerPartFromAssistantMessage } from "@/features/chat/utils";
|
import { getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils";
|
||||||
import { ErrorCode } from "@/lib/errorCodes";
|
import { ErrorCode } from "@/lib/errorCodes";
|
||||||
import { notFound, schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
import { notFound, schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||||
import { isServiceError } from "@/lib/utils";
|
import { isServiceError } from "@/lib/utils";
|
||||||
|
|
@ -49,7 +49,11 @@ export async function POST(req: Request) {
|
||||||
return serviceErrorResponse(schemaValidationError(parsed.error));
|
return serviceErrorResponse(schemaValidationError(parsed.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { messages, id, selectedSearchScopes, languageModelId } = parsed.data;
|
const { messages, id, selectedSearchScopes, languageModel: _languageModel } = parsed.data;
|
||||||
|
// @note: a bit of type massaging is required here since the
|
||||||
|
// zod schema does not enum on `model` or `provider`.
|
||||||
|
// @see: chat/types.ts
|
||||||
|
const languageModel = _languageModel as LanguageModelInfo;
|
||||||
|
|
||||||
const response = await sew(() =>
|
const response = await sew(() =>
|
||||||
withAuth((userId) =>
|
withAuth((userId) =>
|
||||||
|
|
@ -78,13 +82,13 @@ export async function POST(req: Request) {
|
||||||
// corresponding config in `config.json`.
|
// corresponding config in `config.json`.
|
||||||
const languageModelConfig =
|
const languageModelConfig =
|
||||||
(await _getConfiguredLanguageModelsFull())
|
(await _getConfiguredLanguageModelsFull())
|
||||||
.find((model) => model.model === languageModelId);
|
.find((model) => getLanguageModelKey(model) === getLanguageModelKey(languageModel));
|
||||||
|
|
||||||
if (!languageModelConfig) {
|
if (!languageModelConfig) {
|
||||||
return serviceErrorResponse({
|
return serviceErrorResponse({
|
||||||
statusCode: StatusCodes.BAD_REQUEST,
|
statusCode: StatusCodes.BAD_REQUEST,
|
||||||
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
errorCode: ErrorCode.INVALID_REQUEST_BODY,
|
||||||
message: `Language model ${languageModelId} is not configured.`,
|
message: `Language model ${languageModel.model} is not configured.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@
|
||||||
--chat-citation-border: hsl(217, 91%, 60%);
|
--chat-citation-border: hsl(217, 91%, 60%);
|
||||||
|
|
||||||
--warning: #ca8a04;
|
--warning: #ca8a04;
|
||||||
|
--error: #fc5c5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
|
@ -201,6 +202,7 @@
|
||||||
--chat-citation-border: hsl(217, 91%, 60%);
|
--chat-citation-border: hsl(217, 91%, 60%);
|
||||||
|
|
||||||
--warning: #fde047;
|
--warning: #fde047;
|
||||||
|
--error: #f87171;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { AtSignIcon } from "lucide-react";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { ReactEditor, useSlate } from "slate-react";
|
||||||
|
import { AtMentionInfoCard } from "./atMentionInfoCard";
|
||||||
|
|
||||||
|
// @note: we have this as a seperate component to avoid having to re-render the
|
||||||
|
// entire toolbar whenever the user types (since we are using the useSlate hook
|
||||||
|
// here).
|
||||||
|
export const AtMentionButton = () => {
|
||||||
|
const editor = useSlate();
|
||||||
|
|
||||||
|
const onAddContext = useCallback(() => {
|
||||||
|
editor.insertText("@");
|
||||||
|
ReactEditor.focus(editor);
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="w-6 h-6 text-muted-foreground hover:text-primary"
|
||||||
|
onClick={onAddContext}
|
||||||
|
>
|
||||||
|
<AtSignIcon className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" className="p-0 border-0 bg-transparent shadow-none">
|
||||||
|
<AtMentionInfoCard />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -27,6 +27,7 @@ interface ChatBoxProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
isRedirecting?: boolean;
|
isRedirecting?: boolean;
|
||||||
isGenerating?: boolean;
|
isGenerating?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
selectedSearchScopes: SearchScope[];
|
selectedSearchScopes: SearchScope[];
|
||||||
searchContexts: SearchContextQuery[];
|
searchContexts: SearchContextQuery[];
|
||||||
|
|
@ -40,6 +41,7 @@ export const ChatBox = ({
|
||||||
className,
|
className,
|
||||||
isRedirecting,
|
isRedirecting,
|
||||||
isGenerating,
|
isGenerating,
|
||||||
|
isDisabled,
|
||||||
languageModels,
|
languageModels,
|
||||||
selectedSearchScopes,
|
selectedSearchScopes,
|
||||||
searchContexts,
|
searchContexts,
|
||||||
|
|
@ -68,7 +70,7 @@ export const ChatBox = ({
|
||||||
}).flat(),
|
}).flat(),
|
||||||
});
|
});
|
||||||
const { selectedLanguageModel } = useSelectedLanguageModel({
|
const { selectedLanguageModel } = useSelectedLanguageModel({
|
||||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
languageModels,
|
||||||
});
|
});
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
|
@ -167,6 +169,13 @@ export const ChatBox = ({
|
||||||
onContextSelectorOpenChanged(true);
|
onContextSelectorOpenChanged(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSubmitDisabledReason === "no-language-model-selected") {
|
||||||
|
toast({
|
||||||
|
description: "⚠️ You must select a language model",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,6 +296,7 @@ export const ChatBox = ({
|
||||||
renderElement={renderElement}
|
renderElement={renderElement}
|
||||||
renderLeaf={renderLeaf}
|
renderLeaf={renderLeaf}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
readOnly={isDisabled}
|
||||||
/>
|
/>
|
||||||
<div className="ml-auto z-10">
|
<div className="ml-auto z-10">
|
||||||
{isRedirecting ? (
|
{isRedirecting ? (
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,12 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
|
import { LanguageModelInfo, SearchScope } from "@/features/chat/types";
|
||||||
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
||||||
import { AtSignIcon } from "lucide-react";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import { ReactEditor, useSlate } from "slate-react";
|
|
||||||
import { useSelectedLanguageModel } from "../../useSelectedLanguageModel";
|
import { useSelectedLanguageModel } from "../../useSelectedLanguageModel";
|
||||||
|
import { AtMentionButton } from "./atMentionButton";
|
||||||
import { LanguageModelSelector } from "./languageModelSelector";
|
import { LanguageModelSelector } from "./languageModelSelector";
|
||||||
import { SearchScopeSelector } from "./searchScopeSelector";
|
import { SearchScopeSelector } from "./searchScopeSelector";
|
||||||
import { SearchScopeInfoCard } from "@/features/chat/components/chatBox/searchScopeInfoCard";
|
|
||||||
import { AtMentionInfoCard } from "@/features/chat/components/chatBox/atMentionInfoCard";
|
|
||||||
|
|
||||||
export interface ChatBoxToolbarProps {
|
export interface ChatBoxToolbarProps {
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
|
|
@ -33,67 +27,29 @@ export const ChatBoxToolbar = ({
|
||||||
isContextSelectorOpen,
|
isContextSelectorOpen,
|
||||||
onContextSelectorOpenChanged,
|
onContextSelectorOpenChanged,
|
||||||
}: ChatBoxToolbarProps) => {
|
}: ChatBoxToolbarProps) => {
|
||||||
const editor = useSlate();
|
|
||||||
|
|
||||||
const onAddContext = useCallback(() => {
|
|
||||||
editor.insertText("@");
|
|
||||||
ReactEditor.focus(editor);
|
|
||||||
}, [editor]);
|
|
||||||
|
|
||||||
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({
|
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({
|
||||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
languageModels,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip>
|
<AtMentionButton />
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="w-6 h-6 text-muted-foreground hover:text-primary"
|
|
||||||
onClick={onAddContext}
|
|
||||||
>
|
|
||||||
<AtSignIcon className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="bottom" className="p-0 border-0 bg-transparent shadow-none">
|
|
||||||
<AtMentionInfoCard />
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
<Separator orientation="vertical" className="h-3 mx-1" />
|
<Separator orientation="vertical" className="h-3 mx-1" />
|
||||||
<Tooltip>
|
<SearchScopeSelector
|
||||||
<TooltipTrigger asChild>
|
className="bg-inherit w-fit h-6 min-h-6"
|
||||||
<SearchScopeSelector
|
repos={repos}
|
||||||
className="bg-inherit w-fit h-6 min-h-6"
|
searchContexts={searchContexts}
|
||||||
repos={repos}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
searchContexts={searchContexts}
|
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
||||||
selectedSearchScopes={selectedSearchScopes}
|
isOpen={isContextSelectorOpen}
|
||||||
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
onOpenChanged={onContextSelectorOpenChanged}
|
||||||
isOpen={isContextSelectorOpen}
|
/>
|
||||||
onOpenChanged={onContextSelectorOpenChanged}
|
<Separator orientation="vertical" className="h-3 ml-1 mr-2" />
|
||||||
/>
|
<LanguageModelSelector
|
||||||
</TooltipTrigger>
|
languageModels={languageModels}
|
||||||
<TooltipContent side="bottom" className="p-0 border-0 bg-transparent shadow-none">
|
onSelectedModelChange={setSelectedLanguageModel}
|
||||||
<SearchScopeInfoCard />
|
selectedModel={selectedLanguageModel}
|
||||||
</TooltipContent>
|
/>
|
||||||
</Tooltip>
|
|
||||||
{languageModels.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Separator orientation="vertical" className="h-3 ml-1 mr-2" />
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div>
|
|
||||||
<LanguageModelSelector
|
|
||||||
languageModels={languageModels}
|
|
||||||
onSelectedModelChange={setSelectedLanguageModel}
|
|
||||||
selectedModel={selectedLanguageModel}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { BotIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const LanguageModelInfoCard = () => {
|
||||||
|
return (
|
||||||
|
<div className="bg-popover border border-border rounded-lg shadow-lg p-4 w-80 max-w-[90vw]">
|
||||||
|
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-border/50">
|
||||||
|
<BotIcon className="h-4 w-4 text-primary" />
|
||||||
|
<h4 className="text-sm font-semibold text-popover-foreground">Language Model</h4>
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-popover-foreground leading-relaxed">
|
||||||
|
Select the language model to use for the chat. <Link href="https://docs.sourcebot.dev/docs/configuration/language-model-providers" target="_blank" className="text-link">Configuration docs.</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -23,6 +23,9 @@ import {
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { ModelProviderLogo } from "./modelProviderLogo";
|
import { ModelProviderLogo } from "./modelProviderLogo";
|
||||||
|
import { getLanguageModelKey } from "../../utils";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
import { LanguageModelInfoCard } from "./languageModelInfoCard";
|
||||||
|
|
||||||
interface LanguageModelSelectorProps {
|
interface LanguageModelSelectorProps {
|
||||||
languageModels: LanguageModelInfo[];
|
languageModels: LanguageModelInfo[];
|
||||||
|
|
@ -59,7 +62,7 @@ export const LanguageModelSelector = ({
|
||||||
// De-duplicate models
|
// De-duplicate models
|
||||||
const languageModels = useMemo(() => {
|
const languageModels = useMemo(() => {
|
||||||
return _languageModels.filter((model, selfIndex, selfArray) =>
|
return _languageModels.filter((model, selfIndex, selfArray) =>
|
||||||
selfIndex === selfArray.findIndex((t) => t.model === model.model)
|
selfIndex === selfArray.findIndex((t) => getLanguageModelKey(t) === getLanguageModelKey(model))
|
||||||
);
|
);
|
||||||
}, [_languageModels]);
|
}, [_languageModels]);
|
||||||
|
|
||||||
|
|
@ -68,81 +71,89 @@ export const LanguageModelSelector = ({
|
||||||
open={isPopoverOpen}
|
open={isPopoverOpen}
|
||||||
onOpenChange={setIsPopoverOpen}
|
onOpenChange={setIsPopoverOpen}
|
||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
<Tooltip>
|
||||||
<Button
|
<PopoverTrigger asChild>
|
||||||
onClick={handleTogglePopover}
|
<TooltipTrigger asChild>
|
||||||
className={cn(
|
<Button
|
||||||
"flex p-1 rounded-md items-center justify-between bg-inherit h-6",
|
onClick={handleTogglePopover}
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between mx-auto max-w-64 overflow-hidden">
|
|
||||||
{selectedModel ? (
|
|
||||||
<ModelProviderLogo
|
|
||||||
provider={selectedModel.provider}
|
|
||||||
className="mr-1"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Bot className="h-4 w-4 text-muted-foreground mr-1" />
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-sm text-muted-foreground mx-1 text-ellipsis overflow-hidden whitespace-nowrap",
|
"flex p-1 rounded-md items-center justify-between bg-inherit h-6",
|
||||||
selectedModel ? "font-medium" : "font-normal"
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{selectedModel ? (selectedModel.displayName ?? selectedModel.model) : "Select model"}
|
<div className="flex items-center justify-between mx-auto max-w-64 overflow-hidden">
|
||||||
</span>
|
{selectedModel ? (
|
||||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
<ModelProviderLogo
|
||||||
</div>
|
provider={selectedModel.provider}
|
||||||
</Button>
|
className="mr-1"
|
||||||
</PopoverTrigger>
|
/>
|
||||||
<PopoverContent
|
) : (
|
||||||
className="w-auto p-0"
|
<Bot className="h-4 w-4 text-muted-foreground mr-1" />
|
||||||
align="start"
|
)}
|
||||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
<span
|
||||||
>
|
className={cn(
|
||||||
<Command>
|
"text-sm text-muted-foreground mx-1 text-ellipsis overflow-hidden whitespace-nowrap font-medium",
|
||||||
<CommandInput
|
)}
|
||||||
placeholder="Search models..."
|
>
|
||||||
onKeyDown={handleInputKeyDown}
|
{selectedModel ? (selectedModel.displayName ?? selectedModel.model) : "Select model"}
|
||||||
/>
|
</span>
|
||||||
<CommandList>
|
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||||
<CommandEmpty>No models found.</CommandEmpty>
|
</div>
|
||||||
<CommandGroup>
|
</Button>
|
||||||
{languageModels
|
</TooltipTrigger>
|
||||||
.map((model, index) => {
|
</PopoverTrigger>
|
||||||
const isSelected = selectedModel?.model === model.model;
|
<TooltipContent side="bottom" className="p-0 border-0 bg-transparent shadow-none">
|
||||||
return (
|
<LanguageModelInfoCard />
|
||||||
<CommandItem
|
</TooltipContent>
|
||||||
key={`${model.model}-${index}`}
|
<PopoverContent
|
||||||
onSelect={() => {
|
className="w-auto p-0"
|
||||||
selectModel(model)
|
align="start"
|
||||||
}}
|
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||||
className="cursor-pointer"
|
>
|
||||||
>
|
<Command>
|
||||||
<div
|
<CommandInput
|
||||||
className={cn(
|
placeholder="Search models..."
|
||||||
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
onKeyDown={handleInputKeyDown}
|
||||||
isSelected
|
/>
|
||||||
? "bg-primary text-primary-foreground"
|
<CommandList>
|
||||||
: "opacity-50 [&_svg]:invisible"
|
<CommandEmpty>
|
||||||
)}
|
<p>No models found.</p>
|
||||||
>
|
</CommandEmpty>
|
||||||
<CheckIcon className="h-4 w-4" />
|
<CommandGroup>
|
||||||
</div>
|
{languageModels
|
||||||
<ModelProviderLogo
|
.map((model) => {
|
||||||
provider={model.provider}
|
const isSelected = selectedModel && getLanguageModelKey(selectedModel) === getLanguageModelKey(model);
|
||||||
className="mr-2"
|
return (
|
||||||
/>
|
<CommandItem
|
||||||
<span>{model.displayName ?? model.model}</span>
|
key={getLanguageModelKey(model)}
|
||||||
</CommandItem>
|
onSelect={() => {
|
||||||
);
|
selectModel(model)
|
||||||
})}
|
}}
|
||||||
</CommandGroup>
|
className="cursor-pointer"
|
||||||
</CommandList>
|
>
|
||||||
</Command>
|
<div
|
||||||
</PopoverContent>
|
className={cn(
|
||||||
|
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
|
||||||
|
isSelected
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
<ModelProviderLogo
|
||||||
|
provider={model.provider}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
<span>{model.displayName ?? model.model}</span>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Tooltip>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { usePrevious } from '@uidotdev/usehooks';
|
||||||
import { RepositoryQuery, SearchContextQuery } from '@/lib/types';
|
import { RepositoryQuery, SearchContextQuery } from '@/lib/types';
|
||||||
import { generateAndUpdateChatNameFromMessage } from '../../actions';
|
import { generateAndUpdateChatNameFromMessage } from '../../actions';
|
||||||
import { isServiceError } from '@/lib/utils';
|
import { isServiceError } from '@/lib/utils';
|
||||||
|
import { NotConfiguredErrorBanner } from '../notConfiguredErrorBanner';
|
||||||
|
|
||||||
type ChatHistoryState = {
|
type ChatHistoryState = {
|
||||||
scrollOffset?: number;
|
scrollOffset?: number;
|
||||||
|
|
@ -73,7 +74,7 @@ export const ChatThread = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const { selectedLanguageModel } = useSelectedLanguageModel({
|
const { selectedLanguageModel } = useSelectedLanguageModel({
|
||||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
languageModels,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -118,7 +119,7 @@ export const ChatThread = ({
|
||||||
_sendMessage(message, {
|
_sendMessage(message, {
|
||||||
body: {
|
body: {
|
||||||
selectedSearchScopes,
|
selectedSearchScopes,
|
||||||
languageModelId: selectedLanguageModel.model,
|
languageModel: selectedLanguageModel,
|
||||||
} satisfies AdditionalChatRequestParams,
|
} satisfies AdditionalChatRequestParams,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -355,31 +356,38 @@ export const ChatThread = ({
|
||||||
}
|
}
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
{!isChatReadonly && (
|
{!isChatReadonly && (
|
||||||
<div className="border rounded-md w-full max-w-3xl mx-auto mb-8 shadow-sm">
|
<div className="w-full max-w-3xl mx-auto mb-8">
|
||||||
<CustomSlateEditor>
|
{languageModels.length === 0 && (
|
||||||
<ChatBox
|
<NotConfiguredErrorBanner className="mb-2" />
|
||||||
onSubmit={onSubmit}
|
)}
|
||||||
className="min-h-[80px]"
|
|
||||||
preferredSuggestionsBoxPlacement="top-start"
|
<div className="border rounded-md w-full shadow-sm">
|
||||||
isGenerating={status === "streaming" || status === "submitted"}
|
<CustomSlateEditor>
|
||||||
onStop={stop}
|
<ChatBox
|
||||||
languageModels={languageModels}
|
onSubmit={onSubmit}
|
||||||
selectedSearchScopes={selectedSearchScopes}
|
className="min-h-[80px]"
|
||||||
searchContexts={searchContexts}
|
preferredSuggestionsBoxPlacement="top-start"
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
isGenerating={status === "streaming" || status === "submitted"}
|
||||||
/>
|
onStop={stop}
|
||||||
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
|
||||||
<ChatBoxToolbar
|
|
||||||
languageModels={languageModels}
|
languageModels={languageModels}
|
||||||
repos={repos}
|
|
||||||
searchContexts={searchContexts}
|
|
||||||
selectedSearchScopes={selectedSearchScopes}
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
searchContexts={searchContexts}
|
||||||
isContextSelectorOpen={isContextSelectorOpen}
|
|
||||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||||
|
isDisabled={languageModels.length === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
||||||
</CustomSlateEditor>
|
<ChatBoxToolbar
|
||||||
|
languageModels={languageModels}
|
||||||
|
repos={repos}
|
||||||
|
searchContexts={searchContexts}
|
||||||
|
selectedSearchScopes={selectedSearchScopes}
|
||||||
|
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
||||||
|
isContextSelectorOpen={isContextSelectorOpen}
|
||||||
|
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CustomSlateEditor>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { TriangleAlertIcon } from "lucide-react"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const DOCS_URL = "https://docs.sourcebot.dev/docs/configuration/language-model-providers";
|
||||||
|
|
||||||
|
interface NotConfiguredErrorBannerProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NotConfiguredErrorBanner = ({ className }: NotConfiguredErrorBannerProps) => {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-row items-center bg-error rounded-md p-2", className)}>
|
||||||
|
<TriangleAlertIcon className="h-4 w-4 text-accent mr-1.5" />
|
||||||
|
<span className="text-sm font-medium text-accent"><span className="font-bold">Ask unavailable:</span> no language model configured. See the <Link href={DOCS_URL} target="_blank" className="underline">configuration docs</Link> for more information.</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -170,6 +170,13 @@ export type LanguageModelProvider = LanguageModel['provider'];
|
||||||
|
|
||||||
// This is a subset of information about a configured
|
// This is a subset of information about a configured
|
||||||
// language model that we can safely send to the client.
|
// language model that we can safely send to the client.
|
||||||
|
// @note: ensure this is in sync with the LanguageModelInfo type.
|
||||||
|
export const languageModelInfoSchema = z.object({
|
||||||
|
provider: z.string(),
|
||||||
|
model: z.string(),
|
||||||
|
displayName: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export type LanguageModelInfo = {
|
export type LanguageModelInfo = {
|
||||||
provider: LanguageModelProvider,
|
provider: LanguageModelProvider,
|
||||||
model: LanguageModel['model'],
|
model: LanguageModel['model'],
|
||||||
|
|
@ -178,7 +185,7 @@ export type LanguageModelInfo = {
|
||||||
|
|
||||||
// Additional request body data that we send along to the chat API.
|
// Additional request body data that we send along to the chat API.
|
||||||
export const additionalChatRequestParamsSchema = z.object({
|
export const additionalChatRequestParamsSchema = z.object({
|
||||||
languageModelId: z.string(),
|
languageModel: languageModelInfoSchema,
|
||||||
selectedSearchScopes: z.array(searchScopeSchema),
|
selectedSearchScopes: z.array(searchScopeSchema),
|
||||||
});
|
});
|
||||||
export type AdditionalChatRequestParams = z.infer<typeof additionalChatRequestParamsSchema>;
|
export type AdditionalChatRequestParams = z.infer<typeof additionalChatRequestParamsSchema>;
|
||||||
|
|
@ -2,22 +2,40 @@
|
||||||
|
|
||||||
import { useLocalStorage } from "usehooks-ts";
|
import { useLocalStorage } from "usehooks-ts";
|
||||||
import { LanguageModelInfo } from "./types";
|
import { LanguageModelInfo } from "./types";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { getLanguageModelKey } from "./utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialLanguageModel?: LanguageModelInfo;
|
languageModels: LanguageModelInfo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSelectedLanguageModel = ({
|
export const useSelectedLanguageModel = ({
|
||||||
initialLanguageModel,
|
languageModels,
|
||||||
}: Props = {}) => {
|
}: Props) => {
|
||||||
|
const fallbackLanguageModel = languageModels.length > 0 ? languageModels[0] : undefined;
|
||||||
const [selectedLanguageModel, setSelectedLanguageModel] = useLocalStorage<LanguageModelInfo | undefined>(
|
const [selectedLanguageModel, setSelectedLanguageModel] = useLocalStorage<LanguageModelInfo | undefined>(
|
||||||
"selectedLanguageModel",
|
"selectedLanguageModel",
|
||||||
initialLanguageModel,
|
fallbackLanguageModel,
|
||||||
{
|
{
|
||||||
initializeWithValue: false,
|
initializeWithValue: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle the case where the selected language model is no longer
|
||||||
|
// available. Reset to the fallback language model in this case.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedLanguageModel || !languageModels.find(
|
||||||
|
(model) => getLanguageModelKey(model) === getLanguageModelKey(selectedLanguageModel)
|
||||||
|
)) {
|
||||||
|
setSelectedLanguageModel(fallbackLanguageModel);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
fallbackLanguageModel,
|
||||||
|
languageModels,
|
||||||
|
selectedLanguageModel,
|
||||||
|
setSelectedLanguageModel,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedLanguageModel,
|
selectedLanguageModel,
|
||||||
setSelectedLanguageModel,
|
setSelectedLanguageModel,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
CustomText,
|
CustomText,
|
||||||
FileReference,
|
FileReference,
|
||||||
FileSource,
|
FileSource,
|
||||||
|
LanguageModelInfo,
|
||||||
MentionData,
|
MentionData,
|
||||||
MentionElement,
|
MentionElement,
|
||||||
ParagraphElement,
|
ParagraphElement,
|
||||||
|
|
@ -365,4 +366,11 @@ export const buildSearchQuery = (options: {
|
||||||
}
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a unique key given a LanguageModelInfo object.
|
||||||
|
*/
|
||||||
|
export const getLanguageModelKey = (model: LanguageModelInfo) => {
|
||||||
|
return `${model.provider}-${model.model}-${model.displayName}`;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ const config = {
|
||||||
ring: 'var(--sidebar-ring)'
|
ring: 'var(--sidebar-ring)'
|
||||||
},
|
},
|
||||||
warning: 'var(--warning)',
|
warning: 'var(--warning)',
|
||||||
|
error: 'var(--error)',
|
||||||
editor: {
|
editor: {
|
||||||
background: 'var(--editor-background)',
|
background: 'var(--editor-background)',
|
||||||
foreground: 'var(--editor-foreground)',
|
foreground: 'var(--editor-foreground)',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue