mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-11 09:55:59 +00:00
Merge remote-tracking branch 'origin/main' into codex/im-channel-connections
# Conflicts: # backend/app/channels/discord.py # backend/app/channels/manager.py # backend/app/channels/slack.py # backend/app/channels/telegram.py
This commit is contained in:
@@ -18,7 +18,8 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { ComponentProps, HTMLAttributes, ReactElement } from "react";
|
||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import { ClipboardSafeStreamdown } from "./streamdown";
|
||||
|
||||
export type MessageProps = HTMLAttributes<HTMLDivElement> & {
|
||||
from: UIMessage["role"];
|
||||
@@ -302,11 +303,13 @@ export const MessageBranchPage = ({
|
||||
);
|
||||
};
|
||||
|
||||
export type MessageResponseProps = ComponentProps<typeof Streamdown>;
|
||||
export type MessageResponseProps = ComponentProps<
|
||||
typeof ClipboardSafeStreamdown
|
||||
>;
|
||||
|
||||
export const MessageResponse = memo(
|
||||
({ className, ...props }: MessageResponseProps) => (
|
||||
<Streamdown
|
||||
<ClipboardSafeStreamdown
|
||||
className={cn(
|
||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0",
|
||||
className,
|
||||
|
||||
@@ -881,6 +881,7 @@ export type PromptInputTextareaProps = ComponentProps<
|
||||
|
||||
export const PromptInputTextarea = ({
|
||||
onChange,
|
||||
onKeyDown,
|
||||
className,
|
||||
placeholder = "What would you like to know?",
|
||||
...props
|
||||
@@ -891,6 +892,10 @@ export const PromptInputTextarea = ({
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
|
||||
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
onKeyDown?.(e);
|
||||
if (e.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
if (isIMEComposing(e, isComposing)) {
|
||||
return;
|
||||
|
||||
@@ -10,9 +10,9 @@ import { cn } from "@/lib/utils";
|
||||
import { BrainIcon, ChevronDownIcon } from "lucide-react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { createContext, memo, useContext, useEffect, useState } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
import { reasoningPlugins } from "@/core/streamdown/plugins";
|
||||
import { Shimmer } from "./shimmer";
|
||||
import { ClipboardSafeStreamdown } from "./streamdown";
|
||||
|
||||
type ReasoningContextValue = {
|
||||
isStreaming: boolean;
|
||||
@@ -178,7 +178,9 @@ export const ReasoningContent = memo(
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Streamdown {...reasoningPlugins}>{children}</Streamdown>
|
||||
<ClipboardSafeStreamdown {...reasoningPlugins}>
|
||||
{children}
|
||||
</ClipboardSafeStreamdown>
|
||||
</CollapsibleContent>
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { type ComponentProps } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import { installClipboardFallback } from "@/core/clipboard";
|
||||
|
||||
export type ClipboardSafeStreamdownProps = ComponentProps<typeof Streamdown>;
|
||||
|
||||
// Only patch browser globals in client context; skip during SSR
|
||||
if (typeof document !== "undefined") {
|
||||
installClipboardFallback();
|
||||
}
|
||||
|
||||
export function ClipboardSafeStreamdown(props: ClipboardSafeStreamdownProps) {
|
||||
return <Streamdown {...props} />;
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import {
|
||||
Artifact,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
ArtifactHeader,
|
||||
ArtifactTitle,
|
||||
} from "@/components/ai-elements/artifact";
|
||||
import { ClipboardSafeStreamdown } from "@/components/ai-elements/streamdown";
|
||||
import { Select, SelectItem } from "@/components/ui/select";
|
||||
import {
|
||||
SelectContent,
|
||||
@@ -400,13 +400,13 @@ export function ArtifactFilePreview({
|
||||
if (language === "markdown") {
|
||||
return (
|
||||
<div className="size-full px-4">
|
||||
<Streamdown
|
||||
<ClipboardSafeStreamdown
|
||||
className="size-full"
|
||||
{...streamdownPlugins}
|
||||
components={{ a: ArtifactLink }}
|
||||
>
|
||||
{content ?? ""}
|
||||
</Streamdown>
|
||||
</ClipboardSafeStreamdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type KeyboardEvent,
|
||||
} from "react";
|
||||
|
||||
import {
|
||||
@@ -59,6 +60,8 @@ import { fetch } from "@/core/api/fetcher";
|
||||
import { getBackendBaseURL } from "@/core/config";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useModels } from "@/core/models/hooks";
|
||||
import type { Skill } from "@/core/skills";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
import type { AgentThreadContext } from "@/core/threads";
|
||||
import { textOfMessage } from "@/core/threads/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -86,6 +89,48 @@ import { Tooltip } from "./tooltip";
|
||||
|
||||
type InputMode = "flash" | "thinking" | "pro" | "ultra";
|
||||
|
||||
const MAX_SKILL_SUGGESTIONS = 6;
|
||||
|
||||
function getLeadingSlashSkillQuery(value: string): string | null {
|
||||
if (!value.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = value.slice(1);
|
||||
if (query.includes("/") || /\s/.test(query)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function getMatchingSkillSuggestions(skills: Skill[], query: string): Skill[] {
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
|
||||
return skills
|
||||
.map((skill, index) => ({
|
||||
skill,
|
||||
index,
|
||||
name: skill.name.toLowerCase(),
|
||||
}))
|
||||
.filter(({ skill, name }) => {
|
||||
if (!skill.enabled) {
|
||||
return false;
|
||||
}
|
||||
return !normalizedQuery || name.includes(normalizedQuery);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aStartsWith = a.name.startsWith(normalizedQuery);
|
||||
const bStartsWith = b.name.startsWith(normalizedQuery);
|
||||
if (aStartsWith !== bStartsWith) {
|
||||
return aStartsWith ? -1 : 1;
|
||||
}
|
||||
return a.index - b.index;
|
||||
})
|
||||
.slice(0, MAX_SKILL_SUGGESTIONS)
|
||||
.map(({ skill }) => skill);
|
||||
}
|
||||
|
||||
function getResolvedMode(
|
||||
mode: InputMode | undefined,
|
||||
supportsThinking: boolean,
|
||||
@@ -153,11 +198,17 @@ export function InputBox({
|
||||
const { models } = useModels();
|
||||
const { thread, isMock } = useThread();
|
||||
const { textInput } = usePromptInputController();
|
||||
const { skills } = useSkills();
|
||||
const promptRootRef = useRef<HTMLDivElement | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const [followups, setFollowups] = useState<string[]>([]);
|
||||
const [followupsHidden, setFollowupsHidden] = useState(false);
|
||||
const [followupsLoading, setFollowupsLoading] = useState(false);
|
||||
const [textareaFocused, setTextareaFocused] = useState(false);
|
||||
const [skillSuggestionIndex, setSkillSuggestionIndex] = useState(0);
|
||||
const [dismissedSkillSuggestionValue, setDismissedSkillSuggestionValue] =
|
||||
useState<string | null>(null);
|
||||
const lastGeneratedForAiIdRef = useRef<string | null>(null);
|
||||
const wasStreamingRef = useRef(false);
|
||||
const messagesRef = useRef(thread.messages);
|
||||
@@ -347,9 +398,98 @@ export function InputBox({
|
||||
setTimeout(() => requestFormSubmit(), 0);
|
||||
}, [pendingSuggestion, requestFormSubmit, textInput]);
|
||||
|
||||
const slashSkillQuery = useMemo(
|
||||
() => getLeadingSlashSkillQuery(textInput.value ?? ""),
|
||||
[textInput.value],
|
||||
);
|
||||
const skillSuggestions = useMemo(
|
||||
() =>
|
||||
slashSkillQuery === null
|
||||
? []
|
||||
: getMatchingSkillSuggestions(skills, slashSkillQuery),
|
||||
[skills, slashSkillQuery],
|
||||
);
|
||||
const showSkillSuggestions =
|
||||
!disabled &&
|
||||
textareaFocused &&
|
||||
slashSkillQuery !== null &&
|
||||
skillSuggestions.length > 0 &&
|
||||
dismissedSkillSuggestionValue !== textInput.value;
|
||||
|
||||
useEffect(() => {
|
||||
setSkillSuggestionIndex(0);
|
||||
}, [slashSkillQuery, skillSuggestions.length]);
|
||||
|
||||
const applySkillSuggestion = useCallback(
|
||||
(skill: Skill) => {
|
||||
const nextValue = `/${skill.name} `;
|
||||
textInput.setInput(nextValue);
|
||||
setDismissedSkillSuggestionValue(nextValue);
|
||||
requestAnimationFrame(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
textarea.focus();
|
||||
textarea.setSelectionRange(nextValue.length, nextValue.length);
|
||||
});
|
||||
},
|
||||
[textInput],
|
||||
);
|
||||
|
||||
const handleSkillSuggestionKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!showSkillSuggestions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
setSkillSuggestionIndex(
|
||||
(index) => (index + 1) % skillSuggestions.length,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
setSkillSuggestionIndex(
|
||||
(index) =>
|
||||
(index - 1 + skillSuggestions.length) % skillSuggestions.length,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Enter" || event.key === "Tab") {
|
||||
if (event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const selectedSkill = skillSuggestions[skillSuggestionIndex];
|
||||
if (selectedSkill) {
|
||||
applySkillSuggestion(selectedSkill);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
setDismissedSkillSuggestionValue(textInput.value);
|
||||
}
|
||||
},
|
||||
[
|
||||
applySkillSuggestion,
|
||||
showSkillSuggestions,
|
||||
skillSuggestionIndex,
|
||||
skillSuggestions,
|
||||
textInput.value,
|
||||
],
|
||||
);
|
||||
|
||||
const showFollowups =
|
||||
!disabled &&
|
||||
!isWelcomeMode &&
|
||||
!showSkillSuggestions &&
|
||||
!followupsHidden &&
|
||||
(followupsLoading || followups.length > 0);
|
||||
|
||||
@@ -478,6 +618,48 @@ export function InputBox({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showSkillSuggestions && (
|
||||
<div className="absolute right-0 bottom-full left-0 z-40 mb-2 px-1">
|
||||
<div
|
||||
aria-label="Skill suggestions"
|
||||
className="bg-popover/95 text-popover-foreground border-border max-h-72 overflow-y-auto rounded-xl border p-1 shadow-lg backdrop-blur-sm"
|
||||
role="listbox"
|
||||
>
|
||||
{skillSuggestions.map((skill, index) => {
|
||||
const selected = index === skillSuggestionIndex;
|
||||
return (
|
||||
<button
|
||||
aria-selected={selected}
|
||||
className={cn(
|
||||
"flex min-h-12 w-full min-w-0 cursor-pointer items-center gap-3 rounded-lg px-3 py-2 text-left transition-colors",
|
||||
selected
|
||||
? "bg-accent text-accent-foreground"
|
||||
: "text-popover-foreground hover:bg-accent/70 hover:text-accent-foreground",
|
||||
)}
|
||||
key={skill.name}
|
||||
onClick={() => applySkillSuggestion(skill)}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
onMouseEnter={() => setSkillSuggestionIndex(index)}
|
||||
role="option"
|
||||
type="button"
|
||||
>
|
||||
<SparklesIcon className="text-muted-foreground size-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm font-medium">
|
||||
/{skill.name}
|
||||
</span>
|
||||
{skill.description && (
|
||||
<span className="text-muted-foreground block truncate text-xs">
|
||||
{skill.description}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<PromptInput
|
||||
className={cn(
|
||||
"bg-background/85 rounded-2xl backdrop-blur-sm transition-all duration-300 ease-out *:data-[slot='input-group']:rounded-2xl",
|
||||
@@ -506,6 +688,10 @@ export function InputBox({
|
||||
placeholder={t.inputBox.placeholder}
|
||||
autoFocus={autoFocus}
|
||||
defaultValue={initialValue}
|
||||
onBlur={() => setTextareaFocused(false)}
|
||||
onFocus={() => setTextareaFocused(true)}
|
||||
onKeyDown={handleSkillSuggestionKeyDown}
|
||||
ref={textareaRef}
|
||||
/>
|
||||
</PromptInputBody>
|
||||
<PromptInputFooter className="flex">
|
||||
@@ -860,11 +1046,13 @@ export function InputBox({
|
||||
)}
|
||||
</PromptInput>
|
||||
|
||||
{isWelcomeMode && searchParams.get("mode") !== "skill" && (
|
||||
<div className="flex items-center justify-center pt-2">
|
||||
<SuggestionList />
|
||||
</div>
|
||||
)}
|
||||
{isWelcomeMode &&
|
||||
searchParams.get("mode") !== "skill" &&
|
||||
!showSkillSuggestions && (
|
||||
<div className="flex items-center justify-center pt-2">
|
||||
<SuggestionList />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<DialogContent>
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
XCircleIcon,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import {
|
||||
ChainOfThought,
|
||||
@@ -14,6 +13,7 @@ import {
|
||||
ChainOfThoughtStep,
|
||||
} from "@/components/ai-elements/chain-of-thought";
|
||||
import { Shimmer } from "@/components/ai-elements/shimmer";
|
||||
import { ClipboardSafeStreamdown } from "@/components/ai-elements/streamdown";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ShineBorder } from "@/components/ui/shine-border";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
@@ -126,12 +126,12 @@ export function SubtaskCard({
|
||||
{task.prompt && (
|
||||
<ChainOfThoughtStep
|
||||
label={
|
||||
<Streamdown
|
||||
<ClipboardSafeStreamdown
|
||||
{...streamdownPluginsWithWordAnimation}
|
||||
components={{ a: CitationLink }}
|
||||
>
|
||||
{task.prompt}
|
||||
</Streamdown>
|
||||
</ClipboardSafeStreamdown>
|
||||
}
|
||||
></ChainOfThoughtStep>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Streamdown } from "streamdown";
|
||||
import { ClipboardSafeStreamdown } from "@/components/ai-elements/streamdown";
|
||||
|
||||
import { aboutMarkdown } from "./about-content";
|
||||
|
||||
export function AboutSettingsPage() {
|
||||
return <Streamdown>{aboutMarkdown}</Streamdown>;
|
||||
return <ClipboardSafeStreamdown>{aboutMarkdown}</ClipboardSafeStreamdown>;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
import Link from "next/link";
|
||||
import { useDeferredValue, useId, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Streamdown } from "streamdown";
|
||||
|
||||
import { ClipboardSafeStreamdown } from "@/components/ai-elements/streamdown";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -639,12 +639,12 @@ export function MemorySettingsPage() {
|
||||
<div className="text-muted-foreground mb-4 text-sm">
|
||||
{summaryReadOnly}
|
||||
</div>
|
||||
<Streamdown
|
||||
<ClipboardSafeStreamdown
|
||||
className="size-full min-w-0 [overflow-wrap:anywhere] [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
|
||||
{...streamdownPlugins}
|
||||
>
|
||||
{summariesToMarkdown(memory, filteredSectionGroups, t)}
|
||||
</Streamdown>
|
||||
</ClipboardSafeStreamdown>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user