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
|
||||
domain={params.domain}
|
||||
homePath={`/${params.domain}/chat`}
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<span className="text-muted mx-2 select-none">/</span>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { RepositoryQuery, SearchContextQuery } from "@/lib/types";
|
|||
import { useState } from "react";
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { SearchModeSelector } from "../../components/searchModeSelector";
|
||||
import { NotConfiguredErrorBanner } from "@/features/chat/components/notConfiguredErrorBanner";
|
||||
|
||||
interface LandingPageChatBox {
|
||||
languageModels: LanguageModelInfo[];
|
||||
|
|
@ -24,9 +25,13 @@ export const LandingPageChatBox = ({
|
|||
const { createNewChatThread, isLoading } = useCreateNewChatThread();
|
||||
const [selectedSearchScopes, setSelectedSearchScopes] = useLocalStorage<SearchScope[]>("selectedSearchScopes", [], { initializeWithValue: false });
|
||||
const [isContextSelectorOpen, setIsContextSelectorOpen] = useState(false);
|
||||
const isChatBoxDisabled = languageModels.length === 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full">
|
||||
<div className="mt-4 w-full border rounded-md shadow-sm max-w-[800px]">
|
||||
<div className="w-full max-w-[800px] mt-4">
|
||||
|
||||
|
||||
<div className="border rounded-md w-full shadow-sm">
|
||||
<ChatBox
|
||||
onSubmit={(children) => {
|
||||
createNewChatThread(children, selectedSearchScopes);
|
||||
|
|
@ -37,6 +42,7 @@ export const LandingPageChatBox = ({
|
|||
selectedSearchScopes={selectedSearchScopes}
|
||||
searchContexts={searchContexts}
|
||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||
isDisabled={isChatBoxDisabled}
|
||||
/>
|
||||
<Separator />
|
||||
<div className="relative">
|
||||
|
|
@ -57,6 +63,10 @@ export const LandingPageChatBox = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isChatBoxDisabled && (
|
||||
<NotConfiguredErrorBanner className="mt-4" />
|
||||
)}
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
|
@ -8,18 +8,20 @@ import { Separator } from "@/components/ui/separator";
|
|||
interface TopBarProps {
|
||||
domain: string;
|
||||
children?: React.ReactNode;
|
||||
homePath?: string;
|
||||
}
|
||||
|
||||
export const TopBar = ({
|
||||
domain,
|
||||
children,
|
||||
homePath = `/${domain}`,
|
||||
}: TopBarProps) => {
|
||||
return (
|
||||
<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="grow flex flex-row gap-4 items-center">
|
||||
<Link
|
||||
href={`/${domain}`}
|
||||
href={homePath}
|
||||
className="shrink-0 cursor-pointer"
|
||||
>
|
||||
<Image
|
||||
|
|
|
|||
|
|
@ -9,14 +9,8 @@ export default async function Layout(
|
|||
props: LayoutProps
|
||||
) {
|
||||
const params = await props.params;
|
||||
|
||||
const {
|
||||
domain
|
||||
} = params;
|
||||
|
||||
const {
|
||||
children
|
||||
} = props;
|
||||
const { domain } = params;
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { sew, withAuth, withOrgMembership } from "@/actions";
|
||||
import { _getConfiguredLanguageModelsFull, _getAISDKLanguageModelAndOptions, updateChatMessages } from "@/features/chat/actions";
|
||||
import { createAgentStream } from "@/features/chat/agent";
|
||||
import { additionalChatRequestParamsSchema, SBChatMessage, SearchScope } from "@/features/chat/types";
|
||||
import { getAnswerPartFromAssistantMessage } from "@/features/chat/utils";
|
||||
import { additionalChatRequestParamsSchema, LanguageModelInfo, SBChatMessage, SearchScope } from "@/features/chat/types";
|
||||
import { getAnswerPartFromAssistantMessage, getLanguageModelKey } from "@/features/chat/utils";
|
||||
import { ErrorCode } from "@/lib/errorCodes";
|
||||
import { notFound, schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
|
|
@ -49,7 +49,11 @@ export async function POST(req: Request) {
|
|||
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(() =>
|
||||
withAuth((userId) =>
|
||||
|
|
@ -78,13 +82,13 @@ export async function POST(req: Request) {
|
|||
// corresponding config in `config.json`.
|
||||
const languageModelConfig =
|
||||
(await _getConfiguredLanguageModelsFull())
|
||||
.find((model) => model.model === languageModelId);
|
||||
.find((model) => getLanguageModelKey(model) === getLanguageModelKey(languageModel));
|
||||
|
||||
if (!languageModelConfig) {
|
||||
return serviceErrorResponse({
|
||||
statusCode: StatusCodes.BAD_REQUEST,
|
||||
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%);
|
||||
|
||||
--warning: #ca8a04;
|
||||
--error: #fc5c5c;
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
|
@ -201,6 +202,7 @@
|
|||
--chat-citation-border: hsl(217, 91%, 60%);
|
||||
|
||||
--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;
|
||||
isRedirecting?: boolean;
|
||||
isGenerating?: boolean;
|
||||
isDisabled?: boolean;
|
||||
languageModels: LanguageModelInfo[];
|
||||
selectedSearchScopes: SearchScope[];
|
||||
searchContexts: SearchContextQuery[];
|
||||
|
|
@ -40,6 +41,7 @@ export const ChatBox = ({
|
|||
className,
|
||||
isRedirecting,
|
||||
isGenerating,
|
||||
isDisabled,
|
||||
languageModels,
|
||||
selectedSearchScopes,
|
||||
searchContexts,
|
||||
|
|
@ -68,7 +70,7 @@ export const ChatBox = ({
|
|||
}).flat(),
|
||||
});
|
||||
const { selectedLanguageModel } = useSelectedLanguageModel({
|
||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
||||
languageModels,
|
||||
});
|
||||
const { toast } = useToast();
|
||||
|
||||
|
|
@ -167,6 +169,13 @@ export const ChatBox = ({
|
|||
onContextSelectorOpenChanged(true);
|
||||
}
|
||||
|
||||
if (isSubmitDisabledReason === "no-language-model-selected") {
|
||||
toast({
|
||||
description: "⚠️ You must select a language model",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -287,6 +296,7 @@ export const ChatBox = ({
|
|||
renderElement={renderElement}
|
||||
renderLeaf={renderLeaf}
|
||||
onKeyDown={onKeyDown}
|
||||
readOnly={isDisabled}
|
||||
/>
|
||||
<div className="ml-auto z-10">
|
||||
{isRedirecting ? (
|
||||
|
|
|
|||
|
|
@ -1,18 +1,12 @@
|
|||
'use client';
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { LanguageModelInfo, SearchScope } from "@/features/chat/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 { AtMentionButton } from "./atMentionButton";
|
||||
import { LanguageModelSelector } from "./languageModelSelector";
|
||||
import { SearchScopeSelector } from "./searchScopeSelector";
|
||||
import { SearchScopeInfoCard } from "@/features/chat/components/chatBox/searchScopeInfoCard";
|
||||
import { AtMentionInfoCard } from "@/features/chat/components/chatBox/atMentionInfoCard";
|
||||
|
||||
export interface ChatBoxToolbarProps {
|
||||
languageModels: LanguageModelInfo[];
|
||||
|
|
@ -33,67 +27,29 @@ export const ChatBoxToolbar = ({
|
|||
isContextSelectorOpen,
|
||||
onContextSelectorOpenChanged,
|
||||
}: ChatBoxToolbarProps) => {
|
||||
const editor = useSlate();
|
||||
|
||||
const onAddContext = useCallback(() => {
|
||||
editor.insertText("@");
|
||||
ReactEditor.focus(editor);
|
||||
}, [editor]);
|
||||
|
||||
const { selectedLanguageModel, setSelectedLanguageModel } = useSelectedLanguageModel({
|
||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
||||
languageModels,
|
||||
});
|
||||
|
||||
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>
|
||||
<AtMentionButton />
|
||||
<Separator orientation="vertical" className="h-3 mx-1" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SearchScopeSelector
|
||||
className="bg-inherit w-fit h-6 min-h-6"
|
||||
repos={repos}
|
||||
searchContexts={searchContexts}
|
||||
selectedSearchScopes={selectedSearchScopes}
|
||||
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
||||
isOpen={isContextSelectorOpen}
|
||||
onOpenChanged={onContextSelectorOpenChanged}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="p-0 border-0 bg-transparent shadow-none">
|
||||
<SearchScopeInfoCard />
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
<SearchScopeSelector
|
||||
className="bg-inherit w-fit h-6 min-h-6"
|
||||
repos={repos}
|
||||
searchContexts={searchContexts}
|
||||
selectedSearchScopes={selectedSearchScopes}
|
||||
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
||||
isOpen={isContextSelectorOpen}
|
||||
onOpenChanged={onContextSelectorOpenChanged}
|
||||
/>
|
||||
<Separator orientation="vertical" className="h-3 ml-1 mr-2" />
|
||||
<LanguageModelSelector
|
||||
languageModels={languageModels}
|
||||
onSelectedModelChange={setSelectedLanguageModel}
|
||||
selectedModel={selectedLanguageModel}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
import { useMemo, useState } from "react";
|
||||
import { ModelProviderLogo } from "./modelProviderLogo";
|
||||
import { getLanguageModelKey } from "../../utils";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { LanguageModelInfoCard } from "./languageModelInfoCard";
|
||||
|
||||
interface LanguageModelSelectorProps {
|
||||
languageModels: LanguageModelInfo[];
|
||||
|
|
@ -59,7 +62,7 @@ export const LanguageModelSelector = ({
|
|||
// De-duplicate models
|
||||
const languageModels = useMemo(() => {
|
||||
return _languageModels.filter((model, selfIndex, selfArray) =>
|
||||
selfIndex === selfArray.findIndex((t) => t.model === model.model)
|
||||
selfIndex === selfArray.findIndex((t) => getLanguageModelKey(t) === getLanguageModelKey(model))
|
||||
);
|
||||
}, [_languageModels]);
|
||||
|
||||
|
|
@ -68,81 +71,89 @@ export const LanguageModelSelector = ({
|
|||
open={isPopoverOpen}
|
||||
onOpenChange={setIsPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
"flex p-1 rounded-md items-center justify-between bg-inherit h-6",
|
||||
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
|
||||
<Tooltip>
|
||||
<PopoverTrigger asChild>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground mx-1 text-ellipsis overflow-hidden whitespace-nowrap",
|
||||
selectedModel ? "font-medium" : "font-normal"
|
||||
"flex p-1 rounded-md items-center justify-between bg-inherit h-6",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{selectedModel ? (selectedModel.displayName ?? selectedModel.model) : "Select model"}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align="start"
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search models..."
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No models found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{languageModels
|
||||
.map((model, index) => {
|
||||
const isSelected = selectedModel?.model === model.model;
|
||||
return (
|
||||
<CommandItem
|
||||
key={`${model.model}-${index}`}
|
||||
onSelect={() => {
|
||||
selectModel(model)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div
|
||||
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>
|
||||
<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(
|
||||
"text-sm text-muted-foreground mx-1 text-ellipsis overflow-hidden whitespace-nowrap font-medium",
|
||||
)}
|
||||
>
|
||||
{selectedModel ? (selectedModel.displayName ?? selectedModel.model) : "Select model"}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</PopoverTrigger>
|
||||
<TooltipContent side="bottom" className="p-0 border-0 bg-transparent shadow-none">
|
||||
<LanguageModelInfoCard />
|
||||
</TooltipContent>
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align="start"
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<Command>
|
||||
<CommandInput
|
||||
placeholder="Search models..."
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<p>No models found.</p>
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{languageModels
|
||||
.map((model) => {
|
||||
const isSelected = selectedModel && getLanguageModelKey(selectedModel) === getLanguageModelKey(model);
|
||||
return (
|
||||
<CommandItem
|
||||
key={getLanguageModelKey(model)}
|
||||
onSelect={() => {
|
||||
selectModel(model)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { usePrevious } from '@uidotdev/usehooks';
|
|||
import { RepositoryQuery, SearchContextQuery } from '@/lib/types';
|
||||
import { generateAndUpdateChatNameFromMessage } from '../../actions';
|
||||
import { isServiceError } from '@/lib/utils';
|
||||
import { NotConfiguredErrorBanner } from '../notConfiguredErrorBanner';
|
||||
|
||||
type ChatHistoryState = {
|
||||
scrollOffset?: number;
|
||||
|
|
@ -73,7 +74,7 @@ export const ChatThread = ({
|
|||
);
|
||||
|
||||
const { selectedLanguageModel } = useSelectedLanguageModel({
|
||||
initialLanguageModel: languageModels.length > 0 ? languageModels[0] : undefined,
|
||||
languageModels,
|
||||
});
|
||||
|
||||
const {
|
||||
|
|
@ -118,7 +119,7 @@ export const ChatThread = ({
|
|||
_sendMessage(message, {
|
||||
body: {
|
||||
selectedSearchScopes,
|
||||
languageModelId: selectedLanguageModel.model,
|
||||
languageModel: selectedLanguageModel,
|
||||
} satisfies AdditionalChatRequestParams,
|
||||
});
|
||||
|
||||
|
|
@ -355,31 +356,38 @@ export const ChatThread = ({
|
|||
}
|
||||
</ScrollArea>
|
||||
{!isChatReadonly && (
|
||||
<div className="border rounded-md w-full max-w-3xl mx-auto mb-8 shadow-sm">
|
||||
<CustomSlateEditor>
|
||||
<ChatBox
|
||||
onSubmit={onSubmit}
|
||||
className="min-h-[80px]"
|
||||
preferredSuggestionsBoxPlacement="top-start"
|
||||
isGenerating={status === "streaming" || status === "submitted"}
|
||||
onStop={stop}
|
||||
languageModels={languageModels}
|
||||
selectedSearchScopes={selectedSearchScopes}
|
||||
searchContexts={searchContexts}
|
||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||
/>
|
||||
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
||||
<ChatBoxToolbar
|
||||
<div className="w-full max-w-3xl mx-auto mb-8">
|
||||
{languageModels.length === 0 && (
|
||||
<NotConfiguredErrorBanner className="mb-2" />
|
||||
)}
|
||||
|
||||
<div className="border rounded-md w-full shadow-sm">
|
||||
<CustomSlateEditor>
|
||||
<ChatBox
|
||||
onSubmit={onSubmit}
|
||||
className="min-h-[80px]"
|
||||
preferredSuggestionsBoxPlacement="top-start"
|
||||
isGenerating={status === "streaming" || status === "submitted"}
|
||||
onStop={stop}
|
||||
languageModels={languageModels}
|
||||
repos={repos}
|
||||
searchContexts={searchContexts}
|
||||
selectedSearchScopes={selectedSearchScopes}
|
||||
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
||||
isContextSelectorOpen={isContextSelectorOpen}
|
||||
searchContexts={searchContexts}
|
||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||
isDisabled={languageModels.length === 0}
|
||||
/>
|
||||
</div>
|
||||
</CustomSlateEditor>
|
||||
<div className="w-full flex flex-row items-center bg-accent rounded-b-md px-2">
|
||||
<ChatBoxToolbar
|
||||
languageModels={languageModels}
|
||||
repos={repos}
|
||||
searchContexts={searchContexts}
|
||||
selectedSearchScopes={selectedSearchScopes}
|
||||
onSelectedSearchScopesChange={onSelectedSearchScopesChange}
|
||||
isContextSelectorOpen={isContextSelectorOpen}
|
||||
onContextSelectorOpenChanged={setIsContextSelectorOpen}
|
||||
/>
|
||||
</div>
|
||||
</CustomSlateEditor>
|
||||
</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
|
||||
// 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 = {
|
||||
provider: LanguageModelProvider,
|
||||
model: LanguageModel['model'],
|
||||
|
|
@ -178,7 +185,7 @@ export type LanguageModelInfo = {
|
|||
|
||||
// Additional request body data that we send along to the chat API.
|
||||
export const additionalChatRequestParamsSchema = z.object({
|
||||
languageModelId: z.string(),
|
||||
languageModel: languageModelInfoSchema,
|
||||
selectedSearchScopes: z.array(searchScopeSchema),
|
||||
});
|
||||
export type AdditionalChatRequestParams = z.infer<typeof additionalChatRequestParamsSchema>;
|
||||
|
|
@ -2,22 +2,40 @@
|
|||
|
||||
import { useLocalStorage } from "usehooks-ts";
|
||||
import { LanguageModelInfo } from "./types";
|
||||
import { useEffect } from "react";
|
||||
import { getLanguageModelKey } from "./utils";
|
||||
|
||||
type Props = {
|
||||
initialLanguageModel?: LanguageModelInfo;
|
||||
languageModels: LanguageModelInfo[];
|
||||
}
|
||||
|
||||
export const useSelectedLanguageModel = ({
|
||||
initialLanguageModel,
|
||||
}: Props = {}) => {
|
||||
languageModels,
|
||||
}: Props) => {
|
||||
const fallbackLanguageModel = languageModels.length > 0 ? languageModels[0] : undefined;
|
||||
const [selectedLanguageModel, setSelectedLanguageModel] = useLocalStorage<LanguageModelInfo | undefined>(
|
||||
"selectedLanguageModel",
|
||||
initialLanguageModel,
|
||||
fallbackLanguageModel,
|
||||
{
|
||||
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 {
|
||||
selectedLanguageModel,
|
||||
setSelectedLanguageModel,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
CustomText,
|
||||
FileReference,
|
||||
FileSource,
|
||||
LanguageModelInfo,
|
||||
MentionData,
|
||||
MentionElement,
|
||||
ParagraphElement,
|
||||
|
|
@ -366,3 +367,10 @@ export const buildSearchQuery = (options: {
|
|||
|
||||
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)'
|
||||
},
|
||||
warning: 'var(--warning)',
|
||||
error: 'var(--error)',
|
||||
editor: {
|
||||
background: 'var(--editor-background)',
|
||||
foreground: 'var(--editor-foreground)',
|
||||
|
|
|
|||
Loading…
Reference in a new issue