mirror of
https://gitee.com/wanwujie/deer-flow
synced 2026-04-19 12:24:46 +08:00
feat: add suggestions
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user