feat: add suggestions

This commit is contained in:
Henry Li
2026-02-02 11:21:30 +08:00
parent b1227bb911
commit e673405c00
5 changed files with 229 additions and 10 deletions

View File

@@ -1,11 +1,10 @@
"use client";
import { Button } from "@/components/ui/button";
import {
ScrollArea,
ScrollBar,
} from "@/components/ui/scroll-area";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { Icon } from "@radix-ui/react-select";
import type { LucideIcon } from "lucide-react";
import type { ComponentProps } from "react";
export type SuggestionsProps = ComponentProps<typeof ScrollArea>;
@@ -15,7 +14,7 @@ export const Suggestions = ({
children,
...props
}: SuggestionsProps) => (
<ScrollArea className="w-full overflow-x-auto whitespace-nowrap" {...props}>
<ScrollArea className="overflow-x-auto whitespace-nowrap" {...props}>
<div className={cn("flex w-max flex-nowrap items-center gap-2", className)}>
{children}
</div>
@@ -24,32 +23,38 @@ export const Suggestions = ({
);
export type SuggestionProps = Omit<ComponentProps<typeof Button>, "onClick"> & {
suggestion: string;
onClick?: (suggestion: string) => void;
suggestion: React.ReactNode;
icon?: LucideIcon;
onClick?: () => void;
};
export const Suggestion = ({
suggestion,
onClick,
className,
icon: Icon,
variant = "outline",
size = "sm",
children,
...props
}: SuggestionProps) => {
const handleClick = () => {
onClick?.(suggestion);
onClick?.();
};
return (
<Button
className={cn("cursor-pointer rounded-full px-4", className)}
className={cn(
"text-muted-foreground cursor-pointer rounded-full px-4 text-xs font-normal",
className,
)}
onClick={handleClick}
size={size}
type="button"
variant={variant}
{...props}
>
{Icon && <Icon className="size-4" />}
{children || suggestion}
</Button>
);

View File

@@ -6,13 +6,13 @@ import {
GraduationCapIcon,
LightbulbIcon,
PaperclipIcon,
PlusIcon,
ZapIcon,
} from "lucide-react";
import { useCallback, useMemo, useState, type ComponentProps } from "react";
import {
PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu,
PromptInputActionMenuContent,
PromptInputActionMenuItem,
@@ -26,11 +26,13 @@ import {
PromptInputTextarea,
PromptInputTools,
usePromptInputAttachments,
usePromptInputController,
type PromptInputMessage,
} from "@/components/ai-elements/prompt-input";
import {
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks";
@@ -46,6 +48,13 @@ import {
ModelSelectorName,
ModelSelectorTrigger,
} from "../ai-elements/model-selector";
import { Suggestion, Suggestions } from "../ai-elements/suggestion";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { Tooltip } from "./tooltip";
@@ -338,6 +347,11 @@ export function InputBox({
/>
</PromptInputTools>
</PromptInputFooter>
{isNewThread && (
<div className="absolute right-0 -bottom-12 left-0 z-0 flex items-center justify-center">
<SuggestionList />
</div>
)}
{!isNewThread && (
<div className="bg-background absolute right-0 -bottom-[17px] left-0 z-0 h-4"></div>
)}
@@ -345,6 +359,67 @@ export function InputBox({
);
}
function SuggestionList() {
const { t } = useI18n();
const { textInput } = usePromptInputController();
const handleSuggestionClick = useCallback(
(prompt: string | undefined) => {
if (!prompt) return;
textInput.setInput(prompt);
setTimeout(() => {
const textarea = document.querySelector<HTMLTextAreaElement>(
"textarea[name='message']",
);
if (textarea) {
const selStart = prompt.indexOf("[");
const selEnd = prompt.indexOf("]");
if (selStart !== -1 && selEnd !== -1) {
textarea.setSelectionRange(selStart, selEnd + 1);
textarea.focus();
}
}
}, 500);
},
[textInput],
);
return (
<Suggestions className="w-fit">
{t.inputBox.suggestions.map((suggestion) => (
<Suggestion
key={suggestion.suggestion}
icon={suggestion.icon}
suggestion={suggestion.suggestion}
onClick={() => handleSuggestionClick(suggestion.prompt)}
/>
))}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Suggestion icon={PlusIcon} suggestion={t.common.create} />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuGroup>
{t.inputBox.suggestionsCreate.map((suggestion, index) =>
"type" in suggestion && suggestion.type === "separator" ? (
<DropdownMenuSeparator key={index} />
) : (
!("type" in suggestion) && (
<DropdownMenuItem
key={suggestion.suggestion}
onClick={() => handleSuggestionClick(suggestion.prompt)}
>
{suggestion.icon && <suggestion.icon className="size-4" />}
{suggestion.suggestion}
</DropdownMenuItem>
)
),
)}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</Suggestions>
);
}
function AddAttachmentsButton({ className }: { className?: string }) {
const { t } = useI18n();
const attachments = usePromptInputAttachments();