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();

View File

@@ -1,3 +1,14 @@
import {
CompassIcon,
GraduationCapIcon,
ImageIcon,
MicroscopeIcon,
PenLineIcon,
ShapesIcon,
SparklesIcon,
VideoIcon,
} from "lucide-react";
import type { Translations } from "./types";
export const enUS: Translations = {
@@ -29,6 +40,7 @@ export const enUS: Translations = {
cancel: "Cancel",
save: "Save",
install: "Install",
create: "Create",
},
// Welcome
@@ -62,6 +74,55 @@ export const enUS: Translations = {
proModeDescription:
"Reasoning, planning and executing, get more accurate results, may take more time",
searchModels: "Search models...",
suggestions: [
{
suggestion: "Write",
prompt: "Write a blog post about the latest trends on [topic]",
icon: PenLineIcon,
},
{
suggestion: "Research",
prompt:
"Conduct a deep dive research on [topic], and summarize the findings.",
icon: MicroscopeIcon,
},
{
suggestion: "Collect",
prompt: "Collect data from [source] and create a report.",
icon: ShapesIcon,
},
{
suggestion: "Learn",
prompt: "Learn about [topic] and create a tutorial.",
icon: GraduationCapIcon,
},
],
suggestionsCreate: [
{
suggestion: "Webpage",
prompt: "Create a webpage about [topic]",
icon: CompassIcon,
},
{
suggestion: "Image",
prompt: "Create an image about [topic]",
icon: ImageIcon,
},
{
suggestion: "Video",
prompt: "Create a video about [topic]",
icon: VideoIcon,
},
{
type: "separator",
},
{
suggestion: "Skill",
prompt:
"We're going to build a new skill step by step with `skill-creator`. To start, what do you want this skill to do?",
icon: SparklesIcon,
},
],
},
// Sidebar

View File

@@ -1,3 +1,5 @@
import type { LucideIcon } from "lucide-react";
export interface Translations {
// Locale meta
locale: {
@@ -27,6 +29,7 @@ export interface Translations {
cancel: string;
save: string;
install: string;
create: string;
};
// Welcome
@@ -56,6 +59,21 @@ export interface Translations {
proMode: string;
proModeDescription: string;
searchModels: string;
suggestions: {
suggestion: string;
prompt: string;
icon: LucideIcon;
}[];
suggestionsCreate: (
| {
suggestion: string;
prompt: string;
icon: LucideIcon;
}
| {
type: "separator";
}
)[];
};
// Sidebar

View File

@@ -1,3 +1,14 @@
import {
CompassIcon,
GraduationCapIcon,
ImageIcon,
MicroscopeIcon,
PenLineIcon,
ShapesIcon,
SparklesIcon,
VideoIcon,
} from "lucide-react";
import type { Translations } from "./types";
export const zhCN: Translations = {
@@ -29,6 +40,7 @@ export const zhCN: Translations = {
cancel: "取消",
save: "保存",
install: "安装",
create: "创建",
},
// Welcome
@@ -60,6 +72,54 @@ export const zhCN: Translations = {
proMode: "专业",
proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
searchModels: "搜索模型...",
suggestions: [
{
suggestion: "写作",
prompt: "撰写一篇关于[主题]的博客文章",
icon: PenLineIcon,
},
{
suggestion: "研究",
prompt: "深入浅出的研究一下[主题],并总结发现。",
icon: MicroscopeIcon,
},
{
suggestion: "收集",
prompt: "从[来源]收集数据并创建报告。",
icon: ShapesIcon,
},
{
suggestion: "学习",
prompt: "学习关于[主题]并创建教程。",
icon: GraduationCapIcon,
},
],
suggestionsCreate: [
{
suggestion: "网页",
prompt: "生成一个关于[主题]的网页",
icon: CompassIcon,
},
{
suggestion: "图片",
prompt: "生成一个关于[主题]的图片",
icon: ImageIcon,
},
{
suggestion: "视频",
prompt: "生成一个关于[主题]的视频",
icon: VideoIcon,
},
{
type: "separator",
},
{
suggestion: "技能",
prompt:
"我们一起用 skill-creator 技能来创建一个技能吧。先问问我希望这个技能能做什么。",
icon: SparklesIcon,
},
],
},
// Sidebar