mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[Haik]: Remove legacy ChatInput component tree (ChatInput, AttachmentChips, ImageAttachments, useChatSubmit) and replace DashboardToolbar usage with OpenSwarmComposer via a new ToolbarComposer wrapper backed by useStandaloneComposerRuntime and AssistantRuntimeProvider; relocate ModelModeSelector and ContextRing into OpenSwarmComposer/components; move TabLocalState from shared Dashboard types to BrowserCard directory; add embedded prop to OpenSwarmComposer for borderless toolbar rendering
This commit is contained in:
@@ -1,170 +0,0 @@
|
||||
import React, { useState, useRef, useEffect, useId, forwardRef, useImperativeHandle } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import CommandPicker from '@/app/components/CommandPicker';
|
||||
import { useElementSelection } from '@/app/components/ElementSelectionContext';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
import { type AttachedSkill, type TriggerState, EMPTY_TRIGGER, serializeEditorContent } from '@/app/components/richEditorUtils';
|
||||
import { useAppSelector } from '@/shared/hooks';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import type { AttachedImage } from './components/ImageAttachments';
|
||||
import ImageAttachments from './components/ImageAttachments';
|
||||
import type { ForcedToolGroup } from './components/AttachmentChips';
|
||||
import AttachmentChips from './components/AttachmentChips';
|
||||
import ModelModeSelector from '@/app/pages/AgentChat/ModelModeSelector/ModelModeSelector';
|
||||
import { useChatSubmit } from './components/useChatSubmit';
|
||||
|
||||
interface Props {
|
||||
onSend: (message: string, images?: Array<{ data: string; media_type: string }>, contextPaths?: ContextPath[], forcedTools?: string[], attachedSkills?: Array<{ id: string; name: string; content: string }>, selectedBrowserIds?: string[]) => void;
|
||||
disabled?: boolean;
|
||||
mode: string; onModeChange: (mode: string) => void;
|
||||
model: string; onModelChange: (model: string) => void;
|
||||
provider?: string; onProviderChange?: (provider: string) => void;
|
||||
isRunning?: boolean; onStop?: () => void;
|
||||
autoRunMode?: boolean;
|
||||
contextEstimate?: { used: number; limit: number };
|
||||
embedded?: boolean; autoFocus?: boolean;
|
||||
sessionId?: string; queueLength?: number;
|
||||
}
|
||||
|
||||
export interface ChatInputHandle {
|
||||
getConfig: () => { prompt: string; contextPaths: ContextPath[]; forcedTools: ForcedToolGroup[] };
|
||||
setContent: (prompt: string, contextPaths?: ContextPath[], forcedTools?: ForcedToolGroup[]) => void;
|
||||
}
|
||||
|
||||
const ChatInput = forwardRef<ChatInputHandle, Props>(({
|
||||
onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange,
|
||||
isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0,
|
||||
}, ref) => {
|
||||
const c = useClaudeTokens();
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const elementSelection = useElementSelection();
|
||||
|
||||
const fallbackOwnerId = useId();
|
||||
const ownerId = sessionId || fallbackOwnerId;
|
||||
|
||||
useEffect(() => { if (autoFocus) editorRef.current?.focus(); }, [autoFocus]);
|
||||
|
||||
const [hasContent, setHasContent] = useState(false);
|
||||
const [attachedSkills, setAttachedSkills] = useState<Record<string, AttachedSkill>>({});
|
||||
const attachedSkillsRef = useRef(attachedSkills);
|
||||
useEffect(() => { attachedSkillsRef.current = attachedSkills; });
|
||||
const [picker, setPicker] = useState<TriggerState>(EMPTY_TRIGGER);
|
||||
const skills = useAppSelector((state) => state.skills.items);
|
||||
const modesMap = useAppSelector((state) => state.modes.items);
|
||||
|
||||
const [images, setImages] = useState<AttachedImage[]>([]);
|
||||
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null);
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [contextPaths, setContextPaths] = useState<ContextPath[]>([]);
|
||||
const [forcedTools, setForcedTools] = useState<ForcedToolGroup[]>([]);
|
||||
const [copiedPathIdx, setCopiedPathIdx] = useState<number | null>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getConfig: () => {
|
||||
const editor = editorRef.current;
|
||||
const prompt = editor ? serializeEditorContent(editor, attachedSkillsRef.current).trim() : '';
|
||||
return { prompt, contextPaths, forcedTools };
|
||||
},
|
||||
setContent: (prompt: string, newContextPaths?: ContextPath[], newForcedTools?: ForcedToolGroup[]) => {
|
||||
const editor = editorRef.current;
|
||||
if (editor) { editor.textContent = prompt; setHasContent(!!prompt); }
|
||||
if (newContextPaths) setContextPaths(newContextPaths);
|
||||
if (newForcedTools) setForcedTools(newForcedTools);
|
||||
},
|
||||
}), [contextPaths, forcedTools]);
|
||||
|
||||
const {
|
||||
handleSend, handlePickerSelect, handlePaste, handleKeyDown,
|
||||
handleInput, handleEditorClick, handleDragOver, handleDragLeave, handleDrop,
|
||||
addImageFiles, browseAndAttachFiles, removeImage,
|
||||
} = useChatSubmit({
|
||||
editorRef, attachedSkillsRef, disabled, autoRunMode,
|
||||
images, contextPaths, forcedTools, picker, skills, ownerId,
|
||||
elementSelection, onSend, onModeChange, setImages, setContextPaths,
|
||||
setForcedTools, setPicker, setHasContent, setAttachedSkills,
|
||||
setIsDragOver, c,
|
||||
});
|
||||
|
||||
const handleCopyPath = (idx: number) => {
|
||||
navigator.clipboard.writeText(contextPaths[idx].path);
|
||||
setCopiedPathIdx(idx);
|
||||
setTimeout(() => setCopiedPathIdx((cur) => cur === idx ? null : cur), 1200);
|
||||
};
|
||||
|
||||
const selectedElements = elementSelection?.elementsByOwner?.[ownerId] ?? [];
|
||||
const hasAttachments = images.length > 0 || contextPaths.length > 0 || forcedTools.length > 0 || selectedElements.length > 0;
|
||||
const modeLabel = modesMap[mode]?.name || 'Agent';
|
||||
|
||||
return (
|
||||
<Box ref={containerRef} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
...(embedded ? {} : {
|
||||
mx: 1.5, mb: 1.5, borderRadius: '16px',
|
||||
border: isDragOver ? `1px solid ${c.accent.primary}` : `1px solid ${c.border.subtle}`,
|
||||
bgcolor: c.bg.surface, boxShadow: c.shadow.md, transition: 'border-color 0.15s',
|
||||
}),
|
||||
}}>
|
||||
|
||||
{isDragOver && (
|
||||
<Box sx={{ position: 'absolute', inset: 0, bgcolor: 'rgba(174,86,48,0.04)', zIndex: 10,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: '16px', pointerEvents: 'none' }}>
|
||||
<AttachFileIcon sx={{ fontSize: 16, color: c.accent.primary, mr: 0.5 }} />
|
||||
<Typography sx={{ color: c.accent.primary, fontSize: '0.85rem', fontWeight: 500 }}>Drop files here</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<CommandPicker trigger={picker.trigger} filter={picker.filter}
|
||||
onSelect={handlePickerSelect}
|
||||
onClose={() => setPicker((prev) => ({ ...prev, visible: false }))}
|
||||
visible={picker.visible} />
|
||||
|
||||
<ImageAttachments images={images} onRemoveImage={removeImage}
|
||||
lightboxSrc={lightboxSrc} onOpenLightbox={setLightboxSrc} onCloseLightbox={() => setLightboxSrc(null)} c={c} />
|
||||
|
||||
<AttachmentChips contextPaths={contextPaths}
|
||||
onRemoveContextPath={(idx) => setContextPaths((prev) => prev.filter((_, i) => i !== idx))}
|
||||
copiedPathIdx={copiedPathIdx} onCopyPath={handleCopyPath}
|
||||
forcedTools={forcedTools}
|
||||
onRemoveForcedTool={(idx) => setForcedTools((prev) => prev.filter((_, i) => i !== idx))}
|
||||
selectedElements={selectedElements}
|
||||
onRemoveElement={(id) => elementSelection?.removeOwnerElement(ownerId, id)}
|
||||
hasImages={images.length > 0} c={c} />
|
||||
|
||||
<Box sx={{ px: 1.5, pt: hasAttachments ? 0.5 : 1.25, pb: 0.25, position: 'relative' }}>
|
||||
<div ref={editorRef} contentEditable={!disabled} suppressContentEditableWarning
|
||||
onInput={handleInput} onClick={handleEditorClick} onKeyDown={handleKeyDown} onPaste={handlePaste}
|
||||
style={{
|
||||
width: '100%', minHeight: '1.5em', maxHeight: 200, overflowY: 'auto',
|
||||
background: 'transparent', border: 'none', outline: 'none', color: c.text.primary,
|
||||
fontSize: '0.875rem', lineHeight: '1.5', fontFamily: 'inherit',
|
||||
wordBreak: 'break-word', whiteSpace: 'pre-wrap',
|
||||
}} />
|
||||
{!hasContent && (
|
||||
<div style={{
|
||||
position: 'absolute', top: 0, left: 0, right: 0,
|
||||
padding: `${hasAttachments ? 4 : 10}px 12px`,
|
||||
color: c.text.tertiary, fontSize: '0.875rem', lineHeight: '1.5',
|
||||
fontFamily: 'inherit', pointerEvents: 'none', userSelect: 'none',
|
||||
}}>
|
||||
{disabled ? 'Agent is working...' : autoRunMode ? 'Describe what data to generate…' : isRunning ? (queueLength > 0 ? `${queueLength} queued — type another or wait…` : 'Agent is working — messages will queue…') : `${modeLabel}, @ for context, / for commands`}
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ModelModeSelector mode={mode} onModeChange={onModeChange} model={model} onModelChange={onModelChange}
|
||||
provider={provider} onProviderChange={onProviderChange} contextEstimate={contextEstimate}
|
||||
ownerId={ownerId} sessionId={sessionId} autoRunMode={autoRunMode} hasContent={hasContent}
|
||||
isRunning={isRunning} disabled={disabled} onSend={handleSend} onStop={onStop}
|
||||
browseAndAttachFiles={browseAndAttachFiles}
|
||||
queueLength={queueLength} />
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
ChatInput.displayName = 'ChatInput';
|
||||
|
||||
export default ChatInput;
|
||||
@@ -1,118 +0,0 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
|
||||
import AdsClickIcon from '@mui/icons-material/AdsClick';
|
||||
import { getToolGroupIcon } from '@/app/components/CommandPicker';
|
||||
import type { SelectedElement } from '@/app/components/ElementSelectionContext';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
|
||||
export interface ForcedToolGroup {
|
||||
label: string;
|
||||
tools: string[];
|
||||
icon?: React.ReactNode;
|
||||
iconKey?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
contextPaths: ContextPath[];
|
||||
onRemoveContextPath: (idx: number) => void;
|
||||
copiedPathIdx: number | null;
|
||||
onCopyPath: (idx: number) => void;
|
||||
forcedTools: ForcedToolGroup[];
|
||||
onRemoveForcedTool: (idx: number) => void;
|
||||
selectedElements: SelectedElement[];
|
||||
onRemoveElement: (id: string) => void;
|
||||
hasImages: boolean;
|
||||
c: {
|
||||
accent: { primary: string };
|
||||
font: { mono: string };
|
||||
status: { error: string; info: string };
|
||||
};
|
||||
}
|
||||
|
||||
const AttachmentChips: React.FC<Props> = ({
|
||||
contextPaths, onRemoveContextPath, copiedPathIdx, onCopyPath,
|
||||
forcedTools, onRemoveForcedTool,
|
||||
selectedElements, onRemoveElement,
|
||||
hasImages, c,
|
||||
}) => (
|
||||
<>
|
||||
{contextPaths.length > 0 && (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, px: 1.5, pt: hasImages ? 0.25 : 1, pb: 0 }}>
|
||||
{contextPaths.map((cp, idx) => {
|
||||
const label = cp.path.split('/').filter(Boolean).slice(-2).join('/');
|
||||
return (
|
||||
<Tooltip key={`${cp.path}-${idx}`} title={copiedPathIdx === idx ? 'Copied!' : cp.path}
|
||||
arrow placement="top"
|
||||
slotProps={{ tooltip: { sx: { fontFamily: c.font.mono, fontSize: '0.7rem', maxWidth: 420, wordBreak: 'break-all' } } }}>
|
||||
<Chip
|
||||
icon={cp.type === 'directory' ? <FolderOpenIcon sx={{ fontSize: 14 }} /> : <InsertDriveFileOutlinedIcon sx={{ fontSize: 14 }} />}
|
||||
label={label} size="small"
|
||||
onClick={() => onCopyPath(idx)}
|
||||
onDelete={() => onRemoveContextPath(idx)}
|
||||
sx={{
|
||||
bgcolor: `${c.accent.primary}12`, color: c.accent.primary,
|
||||
fontSize: '0.72rem', fontFamily: c.font.mono, height: 26, maxWidth: 220, cursor: 'pointer',
|
||||
'& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
'& .MuiChip-deleteIcon': { color: c.accent.primary, fontSize: 16, '&:hover': { color: c.status.error } },
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{forcedTools.length > 0 && (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, px: 1.5, pt: (hasImages || contextPaths.length > 0) ? 0.25 : 1, pb: 0 }}>
|
||||
{forcedTools.map((ft, idx) => (
|
||||
<Chip key={`ft-${ft.label}-${idx}`}
|
||||
icon={<>{ft.icon || getToolGroupIcon(ft.iconKey || ft.label, 14)}</>}
|
||||
label={`@${ft.label.toLowerCase()}`} size="small"
|
||||
onDelete={() => onRemoveForcedTool(idx)}
|
||||
sx={{
|
||||
bgcolor: `${c.status.info}15`, color: c.status.info,
|
||||
fontSize: '0.72rem', fontFamily: c.font.mono, height: 26, maxWidth: 220,
|
||||
'& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
'& .MuiChip-deleteIcon': { color: c.status.info, fontSize: 16, '&:hover': { color: c.status.error } },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{selectedElements.length > 0 && (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5, px: 1.5,
|
||||
pt: (hasImages || contextPaths.length > 0 || forcedTools.length > 0) ? 0.25 : 1, pb: 0 }}>
|
||||
{selectedElements.map((el) => {
|
||||
const chipLabel = el.semanticLabel ? el.semanticLabel
|
||||
: el.className ? `${el.tagName.toLowerCase()}.${el.className.split(' ')[0]}`
|
||||
: el.tagName.toLowerCase();
|
||||
const tooltipText = el.semanticType
|
||||
? `${el.semanticType}: ${el.semanticLabel || el.selectorPath}`
|
||||
: el.selectorPath;
|
||||
return (
|
||||
<Tooltip key={el.id} title={tooltipText} arrow placement="top"
|
||||
slotProps={{ tooltip: { sx: { fontFamily: c.font.mono, fontSize: '0.7rem', maxWidth: 420, wordBreak: 'break-all' } } }}>
|
||||
<Chip icon={<AdsClickIcon sx={{ fontSize: 14 }} />} label={chipLabel} size="small"
|
||||
onDelete={() => onRemoveElement(el.id)}
|
||||
sx={{
|
||||
bgcolor: 'rgba(59, 130, 246, 0.1)', color: '#3b82f6',
|
||||
fontSize: '0.72rem', fontFamily: c.font.mono, height: 26, maxWidth: 220,
|
||||
'& .MuiChip-label': { overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
'& .MuiChip-deleteIcon': { color: '#3b82f6', fontSize: 16, '&:hover': { color: c.status.error } },
|
||||
'& .MuiChip-icon': { color: '#3b82f6' },
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
export default AttachmentChips;
|
||||
@@ -1,85 +0,0 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Modal from '@mui/material/Modal';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
export interface AttachedImage {
|
||||
data: string;
|
||||
media_type: string;
|
||||
preview: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
images: AttachedImage[];
|
||||
onRemoveImage: (idx: number) => void;
|
||||
lightboxSrc: string | null;
|
||||
onOpenLightbox: (src: string) => void;
|
||||
onCloseLightbox: () => void;
|
||||
c: {
|
||||
border: { subtle: string; medium: string };
|
||||
bg: { surface: string; secondary: string };
|
||||
text: { secondary: string; tertiary: string; primary: string };
|
||||
shadow: { md: string };
|
||||
};
|
||||
}
|
||||
|
||||
const ImageAttachments: React.FC<Props> = ({
|
||||
images, onRemoveImage, lightboxSrc, onOpenLightbox, onCloseLightbox, c,
|
||||
}) => (
|
||||
<>
|
||||
{images.length > 0 && (
|
||||
<Box sx={{
|
||||
display: 'flex', gap: 0.75, px: 1.5, pt: 1, pb: 0.5, overflowX: 'auto',
|
||||
'&::-webkit-scrollbar': { height: 4 },
|
||||
'&::-webkit-scrollbar-thumb': { background: c.border.medium, borderRadius: 2 },
|
||||
}}>
|
||||
{images.map((img, idx) => (
|
||||
<Box key={idx} sx={{
|
||||
position: 'relative', width: 56, height: 56, flexShrink: 0, borderRadius: '8px',
|
||||
overflow: 'hidden', border: `1px solid ${c.border.subtle}`, cursor: 'pointer',
|
||||
transition: 'opacity 0.15s, transform 0.15s',
|
||||
'&:hover': { opacity: 0.85, transform: 'scale(1.04)' },
|
||||
}} onClick={() => onOpenLightbox(img.preview)}>
|
||||
<img src={img.preview} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); onRemoveImage(idx); }} sx={{
|
||||
position: 'absolute', top: -2, right: -2, width: 18, height: 18,
|
||||
bgcolor: c.bg.surface, border: `1px solid ${c.border.medium}`,
|
||||
color: c.text.tertiary,
|
||||
'&:hover': { bgcolor: c.bg.secondary, color: c.text.primary },
|
||||
}}>
|
||||
<CloseIcon sx={{ fontSize: 10 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
open={!!lightboxSrc}
|
||||
onClose={onCloseLightbox}
|
||||
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
<Box onClick={onCloseLightbox} sx={{ position: 'relative', outline: 'none', maxWidth: '90vw', maxHeight: '90vh' }}>
|
||||
<IconButton onClick={onCloseLightbox} sx={{
|
||||
position: 'absolute', top: -16, right: -16, bgcolor: c.bg.surface,
|
||||
border: `1px solid ${c.border.medium}`, color: c.text.secondary,
|
||||
width: 32, height: 32, zIndex: 1, '&:hover': { bgcolor: c.bg.secondary },
|
||||
boxShadow: c.shadow.md,
|
||||
}}>
|
||||
<CloseIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
<img
|
||||
src={lightboxSrc || ''} alt=""
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
maxWidth: '90vw', maxHeight: '90vh', borderRadius: 8,
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.4)', display: 'block',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
export default ImageAttachments;
|
||||
@@ -1,241 +0,0 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import type { CommandPickerItem } from '@/app/components/CommandPicker';
|
||||
import { useElementSelection, type SelectedElement } from '@/app/components/ElementSelectionContext';
|
||||
import { getClipboardCards, clearClipboard } from '@/shared/dashboardClipboard';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
import {
|
||||
SKILL_PILL_ATTR, type AttachedSkill, createSkillPillElement,
|
||||
serializeEditorContent, type TriggerState, detectEditorTrigger,
|
||||
} from '@/app/components/richEditorUtils';
|
||||
import type { AttachedImage } from '../ImageAttachments';
|
||||
import type { ForcedToolGroup } from '../AttachmentChips';
|
||||
|
||||
interface ChatSubmitParams {
|
||||
editorRef: React.RefObject<HTMLDivElement | null>; attachedSkillsRef: React.MutableRefObject<Record<string, AttachedSkill>>;
|
||||
disabled?: boolean; autoRunMode?: boolean; images: AttachedImage[]; contextPaths: ContextPath[];
|
||||
forcedTools: ForcedToolGroup[]; picker: TriggerState;
|
||||
skills: Record<string, { id: string; name: string; content: string }>; ownerId: string;
|
||||
elementSelection: ReturnType<typeof useElementSelection>;
|
||||
onSend: (msg: string, imgs?: Array<{ data: string; media_type: string }>, ctx?: ContextPath[], tools?: string[], skills?: Array<{ id: string; name: string; content: string }>, browserIds?: string[]) => void;
|
||||
onModeChange: (mode: string) => void;
|
||||
setImages: React.Dispatch<React.SetStateAction<AttachedImage[]>>; setContextPaths: React.Dispatch<React.SetStateAction<ContextPath[]>>;
|
||||
setForcedTools: React.Dispatch<React.SetStateAction<ForcedToolGroup[]>>; setPicker: React.Dispatch<React.SetStateAction<TriggerState>>;
|
||||
setHasContent: React.Dispatch<React.SetStateAction<boolean>>; setAttachedSkills: React.Dispatch<React.SetStateAction<Record<string, AttachedSkill>>>;
|
||||
setIsDragOver: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
c: { font: { mono: string }; status: { error: string } };
|
||||
}
|
||||
|
||||
export function useChatSubmit(p: ChatSubmitParams) {
|
||||
const {
|
||||
editorRef, attachedSkillsRef, disabled, autoRunMode,
|
||||
images, contextPaths, forcedTools, picker, skills, ownerId,
|
||||
elementSelection, onSend, onModeChange, setImages, setContextPaths,
|
||||
setForcedTools, setPicker, setHasContent, setAttachedSkills,
|
||||
setIsDragOver, c,
|
||||
} = p;
|
||||
const updateHasContent = useCallback(() => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
const text = (editor.textContent || '').replace(/\u200B/g, '');
|
||||
setHasContent(text.trim().length > 0 || editor.querySelector(`[${SKILL_PILL_ATTR}]`) !== null);
|
||||
}, []);
|
||||
const syncAttachedSkills = useCallback(() => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
const pillIds = new Set(
|
||||
Array.from(editor.querySelectorAll(`[${SKILL_PILL_ATTR}]`))
|
||||
.map((el) => el.getAttribute(SKILL_PILL_ATTR)).filter(Boolean) as string[],
|
||||
);
|
||||
setAttachedSkills((prev) => {
|
||||
const prevKeys = Object.keys(prev);
|
||||
if (prevKeys.length === pillIds.size && prevKeys.every((k) => pillIds.has(k))) return prev;
|
||||
const next: Record<string, AttachedSkill> = {};
|
||||
for (const [id, skill] of Object.entries(prev)) { if (pillIds.has(id)) next[id] = skill; }
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
const removeSkillPill = useCallback((skillId: string) => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
const pill = editor.querySelector(`[${SKILL_PILL_ATTR}="${skillId}"]`);
|
||||
if (pill) pill.remove();
|
||||
setAttachedSkills((prev) => { const { [skillId]: _, ...rest } = prev; return rest; });
|
||||
const text = (editor.textContent || '').replace(/\u200B/g, '');
|
||||
setHasContent(text.trim().length > 0 || editor.querySelector(`[${SKILL_PILL_ATTR}]`) !== null);
|
||||
editor.focus();
|
||||
}, []);
|
||||
const detectTrigger = useCallback(() => { const r = detectEditorTrigger(); r ? setPicker(r) : setPicker((prev) => ({ ...prev, visible: false })); }, []);
|
||||
const handleInput = useCallback(() => { updateHasContent(); detectTrigger(); syncAttachedSkills(); }, [updateHasContent, detectTrigger, syncAttachedSkills]);
|
||||
const handleEditorClick = useCallback(() => { detectTrigger(); }, [detectTrigger]);
|
||||
const addImageFiles = useCallback((files: FileList | File[]) => {
|
||||
Array.from(files).forEach((file) => {
|
||||
if (!file.type.startsWith('image/')) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
setImages((prev) => [...prev, { data: result.split(',')[1], media_type: file.type, preview: result }]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}, []);
|
||||
const attachFiles = useCallback((files: File[]) => {
|
||||
if (files.length === 0) return;
|
||||
const newPaths: ContextPath[] = files
|
||||
.map((f) => (f as File & { path?: string }).path)
|
||||
.filter((p): p is string => Boolean(p))
|
||||
.map((p) => ({ path: p, type: 'file' as const }));
|
||||
if (newPaths.length > 0) setContextPaths((prev) => [...prev, ...newPaths]);
|
||||
}, []);
|
||||
const browseAndAttachFiles = useCallback(async () => {
|
||||
const result = await window.openswarm.showOpenDialog({
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
});
|
||||
if (result.canceled || !result.filePaths?.length) return;
|
||||
const newPaths: ContextPath[] = result.filePaths.map((p) => ({ path: p, type: 'file' as const }));
|
||||
setContextPaths((prev) => [...prev, ...newPaths]);
|
||||
}, []);
|
||||
const handleSend = useCallback(async () => {
|
||||
const editor = editorRef.current;
|
||||
if (!editor || disabled) return;
|
||||
let trimmed = serializeEditorContent(editor, attachedSkillsRef.current).trim();
|
||||
if (!trimmed) return;
|
||||
const selectedEls = elementSelection?.elementsByOwner?.[ownerId] ?? [];
|
||||
let allImages = images.length > 0 ? images.map(({ data, media_type }) => ({ data, media_type })) : [];
|
||||
if (selectedEls.length > 0) {
|
||||
const lines: string[] = ['\n\n---\nSelected UI Elements:\n'];
|
||||
for (let i = 0; i < selectedEls.length; i++) {
|
||||
const el = selectedEls[i];
|
||||
if (el.semanticType === 'browser-card' && el.semanticData?.selectId) {
|
||||
const wv = getWebview(el.semanticData.selectId as string);
|
||||
const url = wv ? (el.semanticData.url || wv.getURL()) : (el.semanticData.url || '');
|
||||
const title = wv ? (el.semanticData.name || wv.getTitle()) : (el.semanticLabel || '');
|
||||
lines.push(`${i + 1}. [Browser Card] ${title}`, ` browser_id: ${el.semanticData.selectId}`);
|
||||
if (url) lines.push(` URL: ${url}`);
|
||||
lines.push(' (Use BrowserAgent with this browser_id to interact with it, or CreateBrowserAgent for a new browser)');
|
||||
} else if (el.semanticType && el.semanticData) {
|
||||
const typeLabel = { 'agent-card': 'Agent Card', message: 'Message', 'tool-call': 'Tool Call', 'tool-group': 'Tool Group', 'view-card': 'App Card', 'browser-card': 'Browser Card', 'dom-element': 'Element' }[el.semanticType] || el.semanticType;
|
||||
lines.push(`${i + 1}. [${typeLabel}] ${el.semanticLabel || ''}`);
|
||||
const { selectId, ...rest } = el.semanticData;
|
||||
if (selectId) lines.push(` ID: ${selectId}`);
|
||||
const metaStr = Object.entries(rest).filter(([, v]) => v != null).map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`).join(', ');
|
||||
if (metaStr) lines.push(` ${metaStr}`);
|
||||
if (el.semanticType === 'agent-card' && selectId) lines.push(` (Use InvokeAgent with session_id "${selectId}" to query this agent with full conversation context)`);
|
||||
} else {
|
||||
const styleStr = Object.entries(el.computedStyles).map(([k, v]) => `${k}: ${v}`).join('; ');
|
||||
lines.push(`${i + 1}. \`${el.selectorPath}\` (${el.tagName.toLowerCase()})`, ` Selector: ${el.selectorPath}`);
|
||||
lines.push(` HTML: ${el.outerHTML.length > 500 ? el.outerHTML.slice(0, 500) + '...' : el.outerHTML}`);
|
||||
if (styleStr) lines.push(` Key styles: ${styleStr}`);
|
||||
}
|
||||
lines.push('');
|
||||
if (el.screenshot) allImages.push({ data: el.screenshot.replace(/^data:image\/\w+;base64,/, ''), media_type: 'image/png' });
|
||||
}
|
||||
trimmed += lines.join('\n');
|
||||
}
|
||||
const allForcedToolNames = forcedTools.flatMap((ft) => ft.tools);
|
||||
const currentSkills = Object.values(attachedSkillsRef.current);
|
||||
const sendSkills = currentSkills.length > 0 ? currentSkills.map((s) => ({ id: s.id, name: s.name, content: s.content })) : undefined;
|
||||
const browserIds = selectedEls.filter((el) => el.semanticType === 'browser-card' && el.semanticData?.selectId).map((el) => el.semanticData!.selectId as string);
|
||||
onSend(trimmed, allImages.length > 0 ? allImages : undefined, contextPaths.length > 0 ? contextPaths : undefined,
|
||||
allForcedToolNames.length > 0 ? allForcedToolNames : undefined, sendSkills, browserIds.length > 0 ? browserIds : undefined);
|
||||
editor.innerHTML = '';
|
||||
setImages([]); setContextPaths([]); setForcedTools([]); setAttachedSkills({}); setHasContent(false);
|
||||
elementSelection?.clearOwnerElements(ownerId);
|
||||
}, [disabled, images, contextPaths, forcedTools, onSend, elementSelection, ownerId]);
|
||||
const handlePickerSelect = (item: CommandPickerItem) => {
|
||||
setPicker((prev) => ({ ...prev, visible: false }));
|
||||
const editor = editorRef.current;
|
||||
if (!editor) return;
|
||||
editor.focus();
|
||||
const { triggerNode, triggerOffset, filter } = picker;
|
||||
if (triggerNode && triggerNode.parentNode && editor.contains(triggerNode)) {
|
||||
const endOffset = Math.min(triggerOffset + 1 + filter.length, triggerNode.length);
|
||||
const range = document.createRange();
|
||||
range.setStart(triggerNode, triggerOffset);
|
||||
range.setEnd(triggerNode, endOffset);
|
||||
range.deleteContents();
|
||||
const sel = window.getSelection();
|
||||
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
|
||||
}
|
||||
if (item.type === 'skill') {
|
||||
const skill = skills[item.id];
|
||||
if (!skill || editor.querySelector(`[${SKILL_PILL_ATTR}="${skill.id}"]`)) return;
|
||||
const pill = createSkillPillElement({ id: skill.id, name: skill.name, content: skill.content }, removeSkillPill, c.font.mono, c.status.error);
|
||||
const sel = window.getSelection();
|
||||
if (sel && sel.rangeCount > 0) {
|
||||
const range = sel.getRangeAt(0);
|
||||
range.collapse(false);
|
||||
range.insertNode(pill);
|
||||
const spacer = document.createTextNode('\u200B');
|
||||
pill.after(spacer);
|
||||
const newRange = document.createRange();
|
||||
newRange.setStartAfter(spacer);
|
||||
newRange.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(newRange);
|
||||
}
|
||||
setAttachedSkills((prev) => ({ ...prev, [skill.id]: { id: skill.id, name: skill.name, content: skill.content } }));
|
||||
} else if (item.type === 'mode') {
|
||||
onModeChange(item.id);
|
||||
} else if (item.type === 'context') {
|
||||
if (item.command === 'file') { browseAndAttachFiles(); return; }
|
||||
else if (item.toolNames && item.toolNames.length > 0) setForcedTools((prev) => [...prev, { label: item.name, tools: item.toolNames!, icon: item.icon, iconKey: item.iconKey }]);
|
||||
else document.execCommand('insertText', false, `@${item.command} `);
|
||||
}
|
||||
updateHasContent();
|
||||
setTimeout(() => editor.focus(), 0);
|
||||
};
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (picker.visible && ['ArrowDown', 'ArrowUp', 'Escape', 'Tab', 'Enter'].includes(e.key)) { e.preventDefault(); return; }
|
||||
if ((e.ctrlKey || e.metaKey) && ['b', 'i', 'u'].includes(e.key.toLowerCase())) { e.preventDefault(); return; }
|
||||
if (e.key === 'Enter' && !e.shiftKey && !autoRunMode) { e.preventDefault(); handleSend(); }
|
||||
};
|
||||
const handlePaste = useCallback((e: React.ClipboardEvent) => {
|
||||
const copied = getClipboardCards();
|
||||
if (copied.length > 0 && elementSelection) {
|
||||
e.preventDefault();
|
||||
for (const card of copied) {
|
||||
const semanticTypeMap: Record<string, SelectedElement['semanticType']> = { agent: 'agent-card', view: 'view-card', browser: 'browser-card' };
|
||||
const semanticType = semanticTypeMap[card.type];
|
||||
if (!semanticType) continue;
|
||||
const labelMap: Record<string, string> = { 'agent-card': 'Agent', 'view-card': 'View', 'browser-card': 'Browser' };
|
||||
const el: SelectedElement = {
|
||||
id: `sel-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
selectorPath: `[data-select-type="${semanticType}"][data-select-id="${card.id}"]`,
|
||||
tagName: 'DIV', className: '', outerHTML: '', computedStyles: {},
|
||||
boundingRect: { x: 0, y: 0, width: 0, height: 0 },
|
||||
semanticType, semanticLabel: (labelMap[semanticType] || semanticType) + ': ' + card.name,
|
||||
semanticData: { ...card.meta, selectId: card.id },
|
||||
};
|
||||
elementSelection.addElementForOwner(ownerId, el);
|
||||
}
|
||||
clearClipboard();
|
||||
return;
|
||||
}
|
||||
const items = e.clipboardData?.items;
|
||||
if (items) {
|
||||
const imageFiles: File[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.startsWith('image/')) { const file = items[i].getAsFile(); if (file) imageFiles.push(file); }
|
||||
}
|
||||
if (imageFiles.length > 0) { e.preventDefault(); addImageFiles(imageFiles); return; }
|
||||
}
|
||||
e.preventDefault();
|
||||
const plain = e.clipboardData?.getData('text/plain');
|
||||
if (plain) document.execCommand('insertText', false, plain);
|
||||
}, [addImageFiles, elementSelection, ownerId]);
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes('Files')) setIsDragOver(true); }, []);
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }, []);
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault(); e.stopPropagation(); setIsDragOver(false);
|
||||
if (e.dataTransfer.files.length === 0) return;
|
||||
const allFiles = Array.from(e.dataTransfer.files);
|
||||
const imageFiles = allFiles.filter((f) => f.type.startsWith('image/'));
|
||||
const otherFiles = allFiles.filter((f) => !f.type.startsWith('image/'));
|
||||
if (imageFiles.length > 0) addImageFiles(imageFiles);
|
||||
if (otherFiles.length > 0) attachFiles(otherFiles);
|
||||
}, [addImageFiles, attachFiles]);
|
||||
const removeImage = useCallback((idx: number) => setImages((prev) => prev.filter((_, i) => i !== idx)), []);
|
||||
|
||||
return { handleSend, handlePickerSelect, handlePaste, handleKeyDown, handleInput, handleEditorClick, handleDragOver, handleDragLeave, handleDrop, addImageFiles, attachFiles, browseAndAttachFiles, removeImage };
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import type { ContextPath } from '@/shared/state/agentsTypes';
|
||||
import { useOpenSwarmMentionAdapter, type MentionItemMetadata } from './components/OpenSwarmMentionAdapter';
|
||||
import { useComposerAttachments } from './components/useComposerAttachments';
|
||||
import { MentionSelectOverride, MentionPopover, ComposerAttachmentChips } from './components/ComposerParts';
|
||||
import ModelModeSelector from '../ModelModeSelector/ModelModeSelector';
|
||||
import ModelModeSelector from './components/ModelModeSelector/ModelModeSelector';
|
||||
|
||||
interface OpenSwarmComposerProps {
|
||||
composerExtrasRef: MutableRefObject<ComposerExtras>;
|
||||
@@ -23,12 +23,13 @@ interface OpenSwarmComposerProps {
|
||||
contextEstimate?: { used: number; limit: number };
|
||||
autoFocus?: boolean;
|
||||
initialContextPaths?: ContextPath[];
|
||||
embedded?: boolean;
|
||||
}
|
||||
|
||||
const OpenSwarmComposer: FC<OpenSwarmComposerProps> = ({
|
||||
composerExtrasRef, mode, onModeChange, model, onModelChange,
|
||||
isRunning, onStop, sessionId, queueLength, contextEstimate, autoFocus,
|
||||
initialContextPaths,
|
||||
initialContextPaths, embedded,
|
||||
}) => {
|
||||
const aui = useAui();
|
||||
const mentionAdapter = useOpenSwarmMentionAdapter();
|
||||
@@ -115,12 +116,15 @@ const OpenSwarmComposer: FC<OpenSwarmComposerProps> = ({
|
||||
att.forcedTools.length > 0 || Object.keys(att.attachedSkills).length > 0;
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-(--thread-max-width) flex-col">
|
||||
<div className={embedded ? 'flex w-full flex-col' : 'mx-auto flex w-full max-w-(--thread-max-width) flex-col'}>
|
||||
<ComposerPrimitive.Unstable_MentionRoot trigger="@" adapter={mentionAdapter}>
|
||||
<ComposerPrimitive.Root ref={formRef} onSubmit={handleFormSubmit} className="aui-composer-root relative flex w-full flex-col">
|
||||
<MentionSelectOverride onSelect={handleMentionSelect} />
|
||||
<div
|
||||
className="flex w-full flex-col gap-1 rounded-2xl border bg-background p-2 transition-shadow focus-within:border-ring/75 focus-within:ring-2 focus-within:ring-ring/20"
|
||||
className={embedded
|
||||
? 'flex w-full flex-col gap-1 bg-transparent p-1'
|
||||
: 'flex w-full flex-col gap-1 rounded-2xl border bg-background p-2 transition-shadow focus-within:border-ring/75 focus-within:ring-2 focus-within:ring-ring/20'
|
||||
}
|
||||
onDragOver={att.handleDragOver} onDragLeave={att.handleDragLeave} onDrop={att.handleDrop}
|
||||
data-dragging={att.isDragOver || undefined}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
useExternalStoreRuntime,
|
||||
type ThreadMessageLike,
|
||||
type AppendMessage,
|
||||
} from '@assistant-ui/react';
|
||||
import type { MutableRefObject } from 'react';
|
||||
import type { ComposerExtras, DispatchableMessage } from './useOpenSwarmRuntime';
|
||||
|
||||
const EMPTY_MESSAGES: ThreadMessageLike[] = [];
|
||||
|
||||
function extractText(message: AppendMessage): string {
|
||||
for (const part of message.content) {
|
||||
if (part.type === 'text') return part.text;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function useStandaloneComposerRuntime(
|
||||
composerExtrasRef: MutableRefObject<ComposerExtras>,
|
||||
dispatchMessage: (msg: DispatchableMessage) => void,
|
||||
) {
|
||||
const onNew = useCallback(
|
||||
async (message: AppendMessage) => {
|
||||
const text = extractText(message);
|
||||
if (!text) return;
|
||||
const extras = composerExtrasRef.current;
|
||||
composerExtrasRef.current = {};
|
||||
dispatchMessage({ prompt: text, ...extras });
|
||||
},
|
||||
[composerExtrasRef, dispatchMessage],
|
||||
);
|
||||
|
||||
return useExternalStoreRuntime({
|
||||
messages: EMPTY_MESSAGES,
|
||||
isRunning: false,
|
||||
onNew,
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import { useOverlayScrollPassthrough } from '../shared/useOverlayScrollPassthrou
|
||||
import { useElementSelection } from '@/app/components/ElementSelectionContext';
|
||||
import { type ResizeDir, CURSOR_MAP, HANDLE_DEFS, DRAG_THRESHOLD } from '../shared/cardLayoutConstants';
|
||||
import { useWebviewLifecycle, isElectron, chromeUserAgent, webviewPreloadPath, type WebviewElement } from './hooks/useWebviewLifecycle';
|
||||
import type { TabLocalState } from '@/app/pages/Dashboard/types/types';
|
||||
import type { TabLocalState } from './TabLocalState';
|
||||
import BrowserTabBar from './components/BrowserTabBar';
|
||||
import BrowserNavBar from './components/BrowserNavBar';
|
||||
import BrowserActionOverlay from './components/BrowserActionOverlay';
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface TabLocalState {
|
||||
loading: boolean;
|
||||
canGoBack: boolean;
|
||||
canGoForward: boolean;
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {
|
||||
reorderBrowserTab, setActiveBrowserTab, addBrowserTab,
|
||||
removeBrowserTab, removeBrowserCard, type BrowserTab,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { TabLocalState } from '@/app/pages/Dashboard/types/types';
|
||||
import type { TabLocalState } from '../TabLocalState';
|
||||
|
||||
interface BrowserTabBarProps {
|
||||
tabs: BrowserTab[];
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import {
|
||||
updateBrowserTabFavicon,
|
||||
type BrowserTab,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { TabLocalState } from '@/app/pages/Dashboard/types/types';
|
||||
import type { TabLocalState } from '../TabLocalState';
|
||||
|
||||
export type WebviewElement = BrowserWebview;
|
||||
|
||||
|
||||
+45
-7
@@ -1,11 +1,52 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import ChatInput from '@/app/pages/AgentChat/ChatInput/ChatInput';
|
||||
import { AssistantRuntimeProvider } from '@assistant-ui/react';
|
||||
import OpenSwarmComposer from '@/app/pages/AgentChat/OpenSwarmComposer/OpenSwarmComposer';
|
||||
import { useStandaloneComposerRuntime } from '@/app/pages/AgentChat/runtime/useStandaloneComposerRuntime';
|
||||
import type { ComposerExtras, DispatchableMessage } from '@/app/pages/AgentChat/runtime/useOpenSwarmRuntime';
|
||||
import { useDashboardToolbar, TOOLBAR_OWNER_ID, ToolbarProps } from './useDashboardToolbar';
|
||||
import HistoryPanel from './components/HistoryPanel';
|
||||
import ViewPickerPanel from './components/ViewPickerPanel';
|
||||
import ToolbarButtons from './components/ToolbarButtons/ToolbarButtons';
|
||||
|
||||
// TODO: pull this out into a separate file or idk do smthn else but not this
|
||||
const ToolbarComposer: React.FC<{
|
||||
mode: string; onModeChange: (mode: string) => void;
|
||||
model: string; onModelChange: (model: string) => void;
|
||||
onSend: (
|
||||
message: string,
|
||||
images?: Array<{ data: string; media_type: string }>,
|
||||
contextPaths?: Array<{ path: string; type: 'file' | 'directory' }>,
|
||||
forcedTools?: string[],
|
||||
attachedSkills?: Array<{ id: string; name: string; content: string }>,
|
||||
selectedBrowserIds?: string[],
|
||||
) => void;
|
||||
}> = ({ mode, onModeChange, model, onModelChange, onSend }) => {
|
||||
const composerExtrasRef = useRef<ComposerExtras>({});
|
||||
const dispatch = useCallback(
|
||||
(msg: DispatchableMessage) => {
|
||||
onSend(msg.prompt, msg.images, msg.contextPaths, msg.forcedTools, msg.attachedSkills, msg.selectedBrowserIds);
|
||||
},
|
||||
[onSend],
|
||||
);
|
||||
const runtime = useStandaloneComposerRuntime(composerExtrasRef, dispatch);
|
||||
|
||||
return (
|
||||
<AssistantRuntimeProvider runtime={runtime}>
|
||||
<OpenSwarmComposer
|
||||
composerExtrasRef={composerExtrasRef}
|
||||
mode={mode}
|
||||
onModeChange={onModeChange}
|
||||
model={model}
|
||||
onModelChange={onModelChange}
|
||||
embedded
|
||||
autoFocus
|
||||
sessionId={TOOLBAR_OWNER_ID}
|
||||
/>
|
||||
</AssistantRuntimeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const DashboardToolbar = React.forwardRef<HTMLDivElement, ToolbarProps>(
|
||||
({ inputOpen, onNewAgent, onCancel, onSend, onAddView, onHistoryResume, onAddBrowser, dashboardId }, ref) => {
|
||||
const {
|
||||
@@ -44,15 +85,12 @@ const DashboardToolbar = React.forwardRef<HTMLDivElement, ToolbarProps>(
|
||||
>
|
||||
{inputOpen ? (
|
||||
<div style={{ width: '100%', minHeight: 44, paddingBottom: 0, marginBottom: -4 }}>
|
||||
<ChatInput
|
||||
onSend={handleSend}
|
||||
<ToolbarComposer
|
||||
mode={mode}
|
||||
onModeChange={setMode}
|
||||
model={model}
|
||||
onModelChange={setModel}
|
||||
embedded
|
||||
autoFocus
|
||||
sessionId={TOOLBAR_OWNER_ID}
|
||||
onSend={handleSend}
|
||||
/>
|
||||
</div>
|
||||
) : historyOpen ? (
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
export interface TabLocalState {
|
||||
loading: boolean;
|
||||
canGoBack: boolean;
|
||||
canGoForward: boolean;
|
||||
}
|
||||
|
||||
export interface TetherInfo {
|
||||
key: string;
|
||||
path: string;
|
||||
|
||||
Reference in New Issue
Block a user