mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-06 01:37:43 +02:00
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PNA9xhjw5SkfuB2tJ5KwCj
311 lines
11 KiB
TypeScript
311 lines
11 KiB
TypeScript
import React, { RefObject, useState } from 'react';
|
|
import Box from '@mui/material/Box';
|
|
import Typography from '@mui/material/Typography';
|
|
import CircularProgress from '@mui/material/CircularProgress';
|
|
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
|
import CommandPicker from '@/app/components/editor/CommandPicker';
|
|
import { useElementSelection } from '@/app/components/editor/ElementSelectionContext';
|
|
import { SelectedElement } from '@/app/components/editor/ElementSelectionContext';
|
|
import { ContextPath } from '@/app/components/editor/DirectoryBrowser';
|
|
import { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
|
import { TriggerState } from '@/app/components/editor/richEditorUtils';
|
|
import { AttachedImage, ForcedToolGroup } from '../types';
|
|
import type { WorkflowsRunContext } from '@/shared/state/dashboardLayoutSlice';
|
|
import { SendBlock } from '../hooks/useContextFiles';
|
|
import { ModelPickerState } from '../hooks/useModelPicker';
|
|
import { SendBlockBanner } from './SendBlockBanner';
|
|
import { AttachmentChips } from './AttachmentChips';
|
|
import { EditorSurface } from './EditorSurface';
|
|
import { ChatInputToolbar } from '../toolbar/ChatInputToolbar';
|
|
import { ChatInputOverlays } from './ChatInputOverlays';
|
|
import { setLastFocusedComposer } from '@/shared/composerFocus';
|
|
|
|
type ThinkingLevel = 'off' | 'low' | 'medium' | 'high' | 'auto';
|
|
interface ModeConf { label: string; icon: React.ReactNode; color: string }
|
|
|
|
interface Props {
|
|
c: ClaudeTokens;
|
|
containerRef: RefObject<HTMLDivElement>;
|
|
editorRef: RefObject<HTMLDivElement>;
|
|
generalFileInputRef: RefObject<HTMLInputElement>;
|
|
embedded?: boolean;
|
|
isDragOver: boolean;
|
|
isUploading: boolean;
|
|
handleDragOver: (e: React.DragEvent) => void;
|
|
handleDragLeave: (e: React.DragEvent) => void;
|
|
handleDrop: (e: React.DragEvent) => void;
|
|
editorPicker: TriggerState;
|
|
setPicker: React.Dispatch<React.SetStateAction<TriggerState>>;
|
|
handlePickerSelect: (item: any) => void;
|
|
sendBlock: SendBlock;
|
|
setSendBlock: (v: SendBlock) => void;
|
|
sessionId?: string;
|
|
images: AttachedImage[];
|
|
setLightboxSrc: (src: string | null) => void;
|
|
removeImage: (idx: number) => void;
|
|
contextPaths: ContextPath[];
|
|
setContextPaths: React.Dispatch<React.SetStateAction<ContextPath[]>>;
|
|
copiedPathIdx: number | null;
|
|
setCopiedPathIdx: React.Dispatch<React.SetStateAction<number | null>>;
|
|
pdfSupported: boolean;
|
|
imageSupported: boolean;
|
|
forcedTools: ForcedToolGroup[];
|
|
setForcedTools: React.Dispatch<React.SetStateAction<ForcedToolGroup[]>>;
|
|
selectedElements: SelectedElement[];
|
|
elementSelection: ReturnType<typeof useElementSelection>;
|
|
ownerId: string;
|
|
disabled?: boolean;
|
|
hasContent: boolean;
|
|
hasAttachments: boolean;
|
|
autoRunMode?: boolean;
|
|
isRunning?: boolean;
|
|
queueLength: number;
|
|
modeConf: ModeConf;
|
|
webSearchOn?: boolean;
|
|
onToggleWebSearch?: () => void;
|
|
placeholderOverride?: string;
|
|
ghostSuggestion?: string;
|
|
runContext?: WorkflowsRunContext;
|
|
onClearRunContext?: () => void;
|
|
handleInput: () => void;
|
|
handleEditorClick: () => void;
|
|
handleKeyDown: (e: React.KeyboardEvent) => void;
|
|
handlePaste: (e: React.ClipboardEvent) => void;
|
|
modesArr: Array<{ id: string; name: string; icon: string; color: string }>;
|
|
mode: string;
|
|
onModeChange: (mode: string) => void;
|
|
iconMap: Record<string, React.ReactNode>;
|
|
modeAnchor: HTMLElement | null;
|
|
setModeAnchor: (el: HTMLElement | null) => void;
|
|
modelAnchor: HTMLElement | null;
|
|
setModelAnchor: (el: HTMLElement | null) => void;
|
|
thinkingAnchor: HTMLElement | null;
|
|
setThinkingAnchor: (el: HTMLElement | null) => void;
|
|
allModelFlat: Array<any>;
|
|
model: string;
|
|
onModelChange: (model: string) => void;
|
|
onProviderChange?: (provider: string) => void;
|
|
picker: ModelPickerState;
|
|
pendingKinds: Set<string>;
|
|
pendingPayloadEstimate: number;
|
|
thinkingLevel: ThinkingLevel;
|
|
onThinkingLevelChange?: (level: ThinkingLevel) => void;
|
|
contextEstimate?: { used: number; limit: number };
|
|
addImageFiles: (files: FileList | File[]) => void;
|
|
uploadAndAttachFiles: (files: File[]) => void;
|
|
onStop?: () => void;
|
|
handleSend: () => void;
|
|
lightboxSrc: string | null;
|
|
oversizeQueue: Array<{ path: string; name: string; tokens: number }>;
|
|
summarizingPath: string | null;
|
|
summarizingAll: boolean;
|
|
summarizeOversize: (path: string) => void;
|
|
summarizeAllOversize: () => void;
|
|
detachOversize: (path: string) => void;
|
|
detachAllOversize: () => void;
|
|
currentModelCtx: number;
|
|
summarizeError: string | null;
|
|
setSummarizeError: (v: string | null) => void;
|
|
}
|
|
|
|
export const ChatInputView: React.FC<Props> = (p) => {
|
|
const { c } = p;
|
|
return (
|
|
<Box
|
|
ref={p.containerRef}
|
|
data-osw-composer={p.sessionId ?? 'dashboard'}
|
|
onFocusCapture={() => setLastFocusedComposer(p.sessionId ?? 'dashboard')}
|
|
onDragOver={p.handleDragOver}
|
|
onDragLeave={p.handleDragLeave}
|
|
onDrop={p.handleDrop}
|
|
sx={{
|
|
position: 'relative',
|
|
...(p.embedded
|
|
? {}
|
|
: {
|
|
mx: 1.5,
|
|
mb: 1.5,
|
|
borderRadius: '16px',
|
|
border: p.isDragOver ? `1px solid ${c.accent.primary}` : `1px solid ${c.border.subtle}`,
|
|
bgcolor: c.bg.surface,
|
|
transition: 'border-color 0.15s',
|
|
}),
|
|
}}
|
|
>
|
|
{p.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.875rem', fontWeight: 500 }}>
|
|
Drop files here
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
{p.isUploading && (
|
|
<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',
|
|
}}
|
|
>
|
|
<CircularProgress size={14} sx={{ color: c.accent.primary, mr: 1 }} />
|
|
<Typography sx={{ color: c.accent.primary, fontSize: '0.875rem', fontWeight: 500 }}>
|
|
Attaching files…
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
<CommandPicker
|
|
trigger={p.editorPicker.trigger}
|
|
filter={p.editorPicker.filter}
|
|
onSelect={p.handlePickerSelect}
|
|
onClose={() => p.setPicker((prev) => ({ ...prev, visible: false }))}
|
|
visible={p.editorPicker.visible}
|
|
/>
|
|
|
|
{p.sendBlock && (
|
|
<SendBlockBanner
|
|
sendBlock={p.sendBlock}
|
|
c={c}
|
|
/>
|
|
)}
|
|
|
|
{p.runContext && (
|
|
<Box sx={{ display: 'flex', mt: 1, mx: 1.5 }}>
|
|
<Box
|
|
title={p.runContext.metaLabel}
|
|
sx={{
|
|
display: 'inline-flex', alignItems: 'center', gap: 0.625, maxWidth: '100%',
|
|
pl: 0.875, pr: 0.5, py: 0.25, borderRadius: '999px',
|
|
bgcolor: c.bg.secondary, border: `1px solid ${c.border.subtle}`,
|
|
}}
|
|
>
|
|
<Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: p.runContext.color, flex: 'none' }} />
|
|
<Typography sx={{ fontSize: '0.6875rem', fontWeight: 600, color: c.text.secondary, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
|
Run attached · {p.runContext.title}
|
|
</Typography>
|
|
<Box
|
|
role="button"
|
|
aria-label="Remove run context"
|
|
onClick={p.onClearRunContext}
|
|
sx={{ width: 15, height: 15, flex: 'none', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: c.text.tertiary, '&:hover': { color: c.text.secondary } }}
|
|
>
|
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4"><path d="M18 6 6 18M6 6l12 12" /></svg>
|
|
</Box>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
|
|
<AttachmentChips
|
|
c={c}
|
|
images={p.images}
|
|
setLightboxSrc={p.setLightboxSrc}
|
|
removeImage={p.removeImage}
|
|
contextPaths={p.contextPaths}
|
|
setContextPaths={p.setContextPaths}
|
|
copiedPathIdx={p.copiedPathIdx}
|
|
setCopiedPathIdx={p.setCopiedPathIdx}
|
|
pdfSupported={p.pdfSupported}
|
|
imageSupported={p.imageSupported}
|
|
forcedTools={p.forcedTools}
|
|
setForcedTools={p.setForcedTools}
|
|
selectedElements={p.selectedElements}
|
|
elementSelection={p.elementSelection}
|
|
ownerId={p.ownerId}
|
|
/>
|
|
|
|
<EditorSurface
|
|
c={c}
|
|
editorRef={p.editorRef}
|
|
disabled={p.disabled}
|
|
hasContent={p.hasContent}
|
|
hasAttachments={p.hasAttachments}
|
|
autoRunMode={p.autoRunMode}
|
|
isRunning={p.isRunning}
|
|
queueLength={p.queueLength}
|
|
placeholderLabel={p.placeholderOverride ?? 'Ask anything, @ for context, / for commands'}
|
|
ghostSuggestion={p.ghostSuggestion}
|
|
onInput={p.handleInput}
|
|
onClick={p.handleEditorClick}
|
|
onKeyDown={p.handleKeyDown}
|
|
onPaste={p.handlePaste}
|
|
/>
|
|
|
|
<ChatInputToolbar
|
|
c={c}
|
|
modeConf={p.modeConf}
|
|
modesArr={p.modesArr}
|
|
mode={p.mode}
|
|
onModeChange={p.onModeChange}
|
|
iconMap={p.iconMap}
|
|
modeAnchor={p.modeAnchor}
|
|
setModeAnchor={p.setModeAnchor}
|
|
modelAnchor={p.modelAnchor}
|
|
setModelAnchor={p.setModelAnchor}
|
|
thinkingAnchor={p.thinkingAnchor}
|
|
setThinkingAnchor={p.setThinkingAnchor}
|
|
allModelFlat={p.allModelFlat}
|
|
model={p.model}
|
|
onModelChange={p.onModelChange}
|
|
onProviderChange={p.onProviderChange}
|
|
picker={p.picker}
|
|
pendingKinds={p.pendingKinds}
|
|
pendingPayloadEstimate={p.pendingPayloadEstimate}
|
|
thinkingLevel={p.thinkingLevel}
|
|
onThinkingLevelChange={p.onThinkingLevelChange}
|
|
contextEstimate={p.contextEstimate}
|
|
elementSelection={p.elementSelection}
|
|
autoRunMode={p.autoRunMode}
|
|
ownerId={p.ownerId}
|
|
sessionId={p.sessionId}
|
|
generalFileInputRef={p.generalFileInputRef}
|
|
addImageFiles={p.addImageFiles}
|
|
uploadAndAttachFiles={p.uploadAndAttachFiles}
|
|
hasContent={p.hasContent}
|
|
disabled={p.disabled}
|
|
isRunning={p.isRunning}
|
|
onStop={p.onStop}
|
|
handleSend={p.handleSend}
|
|
webSearchOn={p.webSearchOn}
|
|
onToggleWebSearch={p.onToggleWebSearch}
|
|
onAttachSkill={(skillId) => p.handlePickerSelect({ id: skillId, type: 'skill', category: '', name: '', description: '', command: '', icon: null })}
|
|
/>
|
|
|
|
<ChatInputOverlays
|
|
c={c}
|
|
lightboxSrc={p.lightboxSrc}
|
|
setLightboxSrc={p.setLightboxSrc}
|
|
oversizeQueue={p.oversizeQueue}
|
|
summarizingPath={p.summarizingPath}
|
|
summarizingAll={p.summarizingAll}
|
|
summarizeOversize={p.summarizeOversize}
|
|
summarizeAllOversize={p.summarizeAllOversize}
|
|
detachOversize={p.detachOversize}
|
|
detachAllOversize={p.detachAllOversize}
|
|
currentModelCtx={p.currentModelCtx}
|
|
summarizeError={p.summarizeError}
|
|
setSummarizeError={p.setSummarizeError}
|
|
/>
|
|
</Box>
|
|
);
|
|
};
|