Make @ mentions clickable

This commit is contained in:
akhisud3195 2025-07-11 21:46:03 +05:30
parent 5f1b85e03b
commit 51adc41096
5 changed files with 335 additions and 266 deletions

View file

@ -23,6 +23,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
atMentions.push({ atMentions.push({
id, id,
value: id, value: id,
label: `Agent: ${a.name}`,
denotationChar: "@", // Add required properties for Match type denotationChar: "@", // Add required properties for Match type
link: id, link: id,
target: "_self" target: "_self"
@ -35,6 +36,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
atMentions.push({ atMentions.push({
id, id,
value: id, value: id,
label: `Prompt: ${prompt.name}`,
denotationChar: "@", denotationChar: "@",
link: id, link: id,
target: "_self" target: "_self"
@ -47,6 +49,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
atMentions.push({ atMentions.push({
id, id,
value: id, value: id,
label: `Tool: ${tool.name}`,
denotationChar: "@", denotationChar: "@",
link: id, link: id,
target: "_self" target: "_self"

View file

@ -7,6 +7,7 @@ import { Label } from "./label";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { Match } from "./mentions_editor"; import { Match } from "./mentions_editor";
import { SparklesIcon } from "lucide-react"; import { SparklesIcon } from "lucide-react";
import { useEntitySelection } from "../../projects/[projectId]/workflow/workflow_editor";
const MentionsEditor = dynamic(() => import('./mentions_editor'), { ssr: false }); const MentionsEditor = dynamic(() => import('./mentions_editor'), { ssr: false });
interface EditableFieldProps { interface EditableFieldProps {
@ -30,6 +31,7 @@ interface EditableFieldProps {
show: boolean; show: boolean;
setShow: (show: boolean) => void; setShow: (show: boolean) => void;
}; };
onMentionNavigate?: (type: 'agent' | 'tool' | 'prompt', name: string) => void;
} }
export function EditableField({ export function EditableField({
@ -50,6 +52,7 @@ export function EditableField({
error, error,
inline = false, inline = false,
showGenerateButton, showGenerateButton,
onMentionNavigate,
}: EditableFieldProps) { }: EditableFieldProps) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [localValue, setLocalValue] = useState(value); const [localValue, setLocalValue] = useState(value);
@ -73,6 +76,18 @@ export function EditableField({
setIsEditing(false); setIsEditing(false);
}); });
let contextMentionNavigate: { onSelectAgent: (name: string) => void; onSelectTool: (name: string) => void; onSelectPrompt: (name: string) => void; } | undefined;
try {
contextMentionNavigate = useEntitySelection();
} catch {}
const handleMentionNavigate = onMentionNavigate || ((type, name) => {
if (contextMentionNavigate) {
if (type === 'agent') contextMentionNavigate.onSelectAgent(name);
else if (type === 'tool') contextMentionNavigate.onSelectTool(name);
else if (type === 'prompt') contextMentionNavigate.onSelectPrompt(name);
}
});
const commonProps = { const commonProps = {
autoFocus: true, autoFocus: true,
value: localValue, value: localValue,
@ -236,10 +251,10 @@ export function EditableField({
{value ? ( {value ? (
<> <>
{markdown && <div> {markdown && <div>
<MarkdownContent content={value} atValues={mentionsAtValues} /> <MarkdownContent content={value} atValues={mentionsAtValues} onMentionNavigate={handleMentionNavigate} />
</div>} </div>}
{!markdown && <div className={multiline ? 'whitespace-pre-wrap' : 'flex items-center'}> {!markdown && <div className={multiline ? 'whitespace-pre-wrap' : 'flex items-center'}>
<MarkdownContent content={value} atValues={mentionsAtValues} /> <MarkdownContent content={value} atValues={mentionsAtValues} onMentionNavigate={handleMentionNavigate} />
</div>} </div>}
</> </>
) : ( ) : (

View file

@ -4,10 +4,12 @@ import { Match } from './mentions_editor';
export default function MarkdownContent({ export default function MarkdownContent({
content, content,
atValues = [] atValues = [],
onMentionNavigate,
}: { }: {
content: string; content: string;
atValues?: Match[]; atValues?: Match[];
onMentionNavigate?: (type: 'agent' | 'tool' | 'prompt', name: string) => void;
}) { }) {
return <div className="overflow-auto break-words"> return <div className="overflow-auto break-words">
<Markdown <Markdown
@ -78,18 +80,45 @@ export default function MarkdownContent({
} }
} }
// Parse type and name for display
let displayLabel = label;
const typeMatch = label.match(/^(agent|tool|prompt):(.*)$/);
let type: 'agent' | 'tool' | 'prompt' | undefined;
let name: string | undefined;
if (typeMatch) {
type = typeMatch[1] as 'agent' | 'tool' | 'prompt';
name = typeMatch[2];
if (type === 'agent') displayLabel = `Agent: ${name}`;
else if (type === 'tool') displayLabel = `Tool: ${name}`;
else if (type === 'prompt') displayLabel = `Prompt: ${name}`;
}
// check if the the mention is valid // check if the the mention is valid
const invalid = !atValues.some(atValue => atValue.id === label); const invalid = !atValues.some(atValue => atValue.id === label);
const handleMentionClick = (e: React.MouseEvent) => {
if (onMentionNavigate && type && name) {
e.preventDefault();
onMentionNavigate(type, name);
}
};
if (atValues.length > 0 && invalid) { if (atValues.length > 0 && invalid) {
return ( return (
<span className="inline-block bg-[#e0f2fe] text-[red] px-1.5 py-0.5 rounded whitespace-nowrap"> <span
@{label} (!) className="inline-block bg-[#e0f2fe] text-[red] px-1.5 py-0.5 rounded whitespace-nowrap cursor-pointer"
onClick={handleMentionClick}
title={onMentionNavigate ? 'Click to open' : undefined}
>
{displayLabel} (!)
</span> </span>
); );
} }
return ( return (
<span className="inline-block bg-[#e0f2fe] text-[#1e40af] px-1.5 py-0.5 rounded whitespace-nowrap"> <span
@{label} className="inline-block bg-[#e0f2fe] text-[#1e40af] px-1.5 py-0.5 rounded whitespace-nowrap cursor-pointer"
onClick={handleMentionClick}
title={onMentionNavigate ? 'Click to open' : undefined}
>
{displayLabel}
</span> </span>
); );
} }

View file

@ -11,6 +11,7 @@ export type Match = {
id: string; id: string;
value: string; value: string;
invalid?: boolean; invalid?: boolean;
label?: string;
[key: string]: string | boolean | undefined; [key: string]: string | boolean | undefined;
}; };
@ -18,7 +19,7 @@ class CustomMentionBlot extends MentionBlot {
static render(data: any) { static render(data: any) {
const element = document.createElement('span'); const element = document.createElement('span');
element.className = data.invalid ? 'invalid' : ''; element.className = data.invalid ? 'invalid' : '';
element.textContent = data.invalid ? `${data.value} (!)` : data.value; element.textContent = data.invalid ? `${data.label || data.value} (!)` : (data.label || data.value);
return element; return element;
} }
} }
@ -154,7 +155,7 @@ export default function MentionEditor({
renderItem: (item: Match) => { renderItem: (item: Match) => {
const div = document.createElement('div'); const div = document.createElement('div');
div.className = "px-2 py-1 bg-white text-blue-800 hover:bg-blue-100 cursor-pointer"; div.className = "px-2 py-1 bg-white text-blue-800 hover:bg-blue-100 cursor-pointer";
div.textContent = item.id; div.textContent = item.label || item.id;
return div; return div;
}, },
} }

View file

@ -553,6 +553,19 @@ function reducer(state: State, action: Action): State {
return newState; return newState;
} }
// Context for entity selection
export const EntitySelectionContext = createContext<{
onSelectAgent: (name: string) => void;
onSelectTool: (name: string) => void;
onSelectPrompt: (name: string) => void;
} | null>(null);
export function useEntitySelection() {
const ctx = useContext(EntitySelectionContext);
if (!ctx) throw new Error('useEntitySelection must be used within EntitySelectionContext');
return ctx;
}
export function WorkflowEditor({ export function WorkflowEditor({
dataSources, dataSources,
workflow, workflow,
@ -819,7 +832,13 @@ export function WorkflowEditor({
setIsInitialState(false); setIsInitialState(false);
} }
return <div className="flex flex-col h-full relative"> return (
<EntitySelectionContext.Provider value={{
onSelectAgent: handleSelectAgent,
onSelectTool: handleSelectTool,
onSelectPrompt: handleSelectPrompt,
}}>
<div className="flex flex-col h-full relative">
<div className="shrink-0 flex justify-between items-center pb-6"> <div className="shrink-0 flex justify-between items-center pb-6">
<div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100"> <div className="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
<WorkflowIcon size={16} /> <WorkflowIcon size={16} />
@ -1090,5 +1109,7 @@ export function WorkflowEditor({
onComplete={() => setShowTour(false)} onComplete={() => setShowTour(false)}
/> />
)} )}
</div>; </div>
</EntitySelectionContext.Provider>
);
} }