feat: add suggestions

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

View File

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

View File

@@ -6,13 +6,13 @@ import {
GraduationCapIcon, GraduationCapIcon,
LightbulbIcon, LightbulbIcon,
PaperclipIcon, PaperclipIcon,
PlusIcon,
ZapIcon, ZapIcon,
} from "lucide-react"; } from "lucide-react";
import { useCallback, useMemo, useState, type ComponentProps } from "react"; import { useCallback, useMemo, useState, type ComponentProps } from "react";
import { import {
PromptInput, PromptInput,
PromptInputActionAddAttachments,
PromptInputActionMenu, PromptInputActionMenu,
PromptInputActionMenuContent, PromptInputActionMenuContent,
PromptInputActionMenuItem, PromptInputActionMenuItem,
@@ -26,11 +26,13 @@ import {
PromptInputTextarea, PromptInputTextarea,
PromptInputTools, PromptInputTools,
usePromptInputAttachments, usePromptInputAttachments,
usePromptInputController,
type PromptInputMessage, type PromptInputMessage,
} from "@/components/ai-elements/prompt-input"; } from "@/components/ai-elements/prompt-input";
import { import {
DropdownMenuGroup, DropdownMenuGroup,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { useModels } from "@/core/models/hooks"; import { useModels } from "@/core/models/hooks";
@@ -46,6 +48,13 @@ import {
ModelSelectorName, ModelSelectorName,
ModelSelectorTrigger, ModelSelectorTrigger,
} from "../ai-elements/model-selector"; } 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"; import { Tooltip } from "./tooltip";
@@ -338,6 +347,11 @@ export function InputBox({
/> />
</PromptInputTools> </PromptInputTools>
</PromptInputFooter> </PromptInputFooter>
{isNewThread && (
<div className="absolute right-0 -bottom-12 left-0 z-0 flex items-center justify-center">
<SuggestionList />
</div>
)}
{!isNewThread && ( {!isNewThread && (
<div className="bg-background absolute right-0 -bottom-[17px] left-0 z-0 h-4"></div> <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 }) { function AddAttachmentsButton({ className }: { className?: string }) {
const { t } = useI18n(); const { t } = useI18n();
const attachments = usePromptInputAttachments(); 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"; import type { Translations } from "./types";
export const enUS: Translations = { export const enUS: Translations = {
@@ -29,6 +40,7 @@ export const enUS: Translations = {
cancel: "Cancel", cancel: "Cancel",
save: "Save", save: "Save",
install: "Install", install: "Install",
create: "Create",
}, },
// Welcome // Welcome
@@ -62,6 +74,55 @@ export const enUS: Translations = {
proModeDescription: proModeDescription:
"Reasoning, planning and executing, get more accurate results, may take more time", "Reasoning, planning and executing, get more accurate results, may take more time",
searchModels: "Search models...", 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 // Sidebar

View File

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

View File

@@ -1,3 +1,14 @@
import {
CompassIcon,
GraduationCapIcon,
ImageIcon,
MicroscopeIcon,
PenLineIcon,
ShapesIcon,
SparklesIcon,
VideoIcon,
} from "lucide-react";
import type { Translations } from "./types"; import type { Translations } from "./types";
export const zhCN: Translations = { export const zhCN: Translations = {
@@ -29,6 +40,7 @@ export const zhCN: Translations = {
cancel: "取消", cancel: "取消",
save: "保存", save: "保存",
install: "安装", install: "安装",
create: "创建",
}, },
// Welcome // Welcome
@@ -60,6 +72,54 @@ export const zhCN: Translations = {
proMode: "专业", proMode: "专业",
proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间", proModeDescription: "思考、计划再执行,获得更精准的结果,可能需要更多时间",
searchModels: "搜索模型...", 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 // Sidebar