mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-13 17:22:37 +02:00
Make @ mentions clickable
This commit is contained in:
parent
5f1b85e03b
commit
51adc41096
5 changed files with 335 additions and 266 deletions
|
|
@ -23,6 +23,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
|
|||
atMentions.push({
|
||||
id,
|
||||
value: id,
|
||||
label: `Agent: ${a.name}`,
|
||||
denotationChar: "@", // Add required properties for Match type
|
||||
link: id,
|
||||
target: "_self"
|
||||
|
|
@ -35,6 +36,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
|
|||
atMentions.push({
|
||||
id,
|
||||
value: id,
|
||||
label: `Prompt: ${prompt.name}`,
|
||||
denotationChar: "@",
|
||||
link: id,
|
||||
target: "_self"
|
||||
|
|
@ -47,6 +49,7 @@ export function createAtMentions({ agents, prompts, tools, currentAgentName }: C
|
|||
atMentions.push({
|
||||
id,
|
||||
value: id,
|
||||
label: `Tool: ${tool.name}`,
|
||||
denotationChar: "@",
|
||||
link: id,
|
||||
target: "_self"
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Label } from "./label";
|
|||
import dynamic from "next/dynamic";
|
||||
import { Match } from "./mentions_editor";
|
||||
import { SparklesIcon } from "lucide-react";
|
||||
import { useEntitySelection } from "../../projects/[projectId]/workflow/workflow_editor";
|
||||
const MentionsEditor = dynamic(() => import('./mentions_editor'), { ssr: false });
|
||||
|
||||
interface EditableFieldProps {
|
||||
|
|
@ -30,6 +31,7 @@ interface EditableFieldProps {
|
|||
show: boolean;
|
||||
setShow: (show: boolean) => void;
|
||||
};
|
||||
onMentionNavigate?: (type: 'agent' | 'tool' | 'prompt', name: string) => void;
|
||||
}
|
||||
|
||||
export function EditableField({
|
||||
|
|
@ -50,6 +52,7 @@ export function EditableField({
|
|||
error,
|
||||
inline = false,
|
||||
showGenerateButton,
|
||||
onMentionNavigate,
|
||||
}: EditableFieldProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
|
|
@ -73,6 +76,18 @@ export function EditableField({
|
|||
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 = {
|
||||
autoFocus: true,
|
||||
value: localValue,
|
||||
|
|
@ -236,10 +251,10 @@ export function EditableField({
|
|||
{value ? (
|
||||
<>
|
||||
{markdown && <div>
|
||||
<MarkdownContent content={value} atValues={mentionsAtValues} />
|
||||
<MarkdownContent content={value} atValues={mentionsAtValues} onMentionNavigate={handleMentionNavigate} />
|
||||
</div>}
|
||||
{!markdown && <div className={multiline ? 'whitespace-pre-wrap' : 'flex items-center'}>
|
||||
<MarkdownContent content={value} atValues={mentionsAtValues} />
|
||||
<MarkdownContent content={value} atValues={mentionsAtValues} onMentionNavigate={handleMentionNavigate} />
|
||||
</div>}
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import { Match } from './mentions_editor';
|
|||
|
||||
export default function MarkdownContent({
|
||||
content,
|
||||
atValues = []
|
||||
atValues = [],
|
||||
onMentionNavigate,
|
||||
}: {
|
||||
content: string;
|
||||
atValues?: Match[];
|
||||
onMentionNavigate?: (type: 'agent' | 'tool' | 'prompt', name: string) => void;
|
||||
}) {
|
||||
return <div className="overflow-auto break-words">
|
||||
<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
|
||||
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) {
|
||||
return (
|
||||
<span className="inline-block bg-[#e0f2fe] text-[red] px-1.5 py-0.5 rounded whitespace-nowrap">
|
||||
@{label} (!)
|
||||
<span
|
||||
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>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="inline-block bg-[#e0f2fe] text-[#1e40af] px-1.5 py-0.5 rounded whitespace-nowrap">
|
||||
@{label}
|
||||
<span
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export type Match = {
|
|||
id: string;
|
||||
value: string;
|
||||
invalid?: boolean;
|
||||
label?: string;
|
||||
[key: string]: string | boolean | undefined;
|
||||
};
|
||||
|
||||
|
|
@ -18,7 +19,7 @@ class CustomMentionBlot extends MentionBlot {
|
|||
static render(data: any) {
|
||||
const element = document.createElement('span');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -154,7 +155,7 @@ export default function MentionEditor({
|
|||
renderItem: (item: Match) => {
|
||||
const div = document.createElement('div');
|
||||
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;
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -553,6 +553,19 @@ function reducer(state: State, action: Action): State {
|
|||
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({
|
||||
dataSources,
|
||||
workflow,
|
||||
|
|
@ -819,7 +832,13 @@ export function WorkflowEditor({
|
|||
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="workflow-version-selector flex items-center gap-4 px-2 text-gray-800 dark:text-gray-100">
|
||||
<WorkflowIcon size={16} />
|
||||
|
|
@ -1090,5 +1109,7 @@ export function WorkflowEditor({
|
|||
onComplete={() => setShowTour(false)}
|
||||
/>
|
||||
)}
|
||||
</div>;
|
||||
</div>
|
||||
</EntitySelectionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue