diff --git a/frontend/src/app/components/domSelectorHelpers.ts b/frontend/src/app/components/domSelectorHelpers.ts index a6bb33db..e43023e8 100644 --- a/frontend/src/app/components/domSelectorHelpers.ts +++ b/frontend/src/app/components/domSelectorHelpers.ts @@ -5,7 +5,7 @@ export const SELECT_ID_ATTR = 'data-select-id'; export const SELECT_META_ATTR = 'data-select-meta'; const DRAG_SELECT_TYPES = ['agent-card', 'view-card', 'browser-card'] as const; -export const DRAG_SELECTOR = DRAG_SELECT_TYPES.map((t) => `[${SELECT_ATTR}="${t}"]`).join(','); +const DRAG_SELECTOR = DRAG_SELECT_TYPES.map((t) => `[${SELECT_ATTR}="${t}"]`).join(','); export interface OverlayState { visible: boolean; @@ -27,6 +27,15 @@ export interface DragRect { export const EMPTY_OVERLAY: OverlayState = { visible: false, top: 0, left: 0, width: 0, height: 0, label: '' }; export const EMPTY_DRAG: DragRect = { visible: false, top: 0, left: 0, width: 0, height: 0 }; +export interface SelectMeta { + name?: string; + role?: string; + content?: string; + label?: string; + tool?: string; + [key: string]: unknown; +} + const SEMANTIC_LABELS: Record = { 'agent-card': 'Agent', 'message': 'Message', @@ -48,7 +57,7 @@ export function findSelectableAncestor(target: Element, excludeId?: string | nul return null; } -export function buildSemanticLabel(type: string, meta: Record): string { +export function buildSemanticLabel(type: string, meta: SelectMeta): string { const prefix = SEMANTIC_LABELS[type] || type; if (meta.name) return `${prefix}: ${meta.name}`; if (meta.role && meta.content) { @@ -60,7 +69,7 @@ export function buildSemanticLabel(type: string, meta: Record): str return prefix; } -export function rectsIntersect( +function rectsIntersect( a: { top: number; left: number; bottom: number; right: number }, b: { top: number; left: number; bottom: number; right: number }, ): boolean { @@ -70,8 +79,8 @@ export function rectsIntersect( export function buildSelectedElement(el: Element): SelectedElement { const type = el.getAttribute(SELECT_ATTR) || ''; const selectId = el.getAttribute(SELECT_ID_ATTR) || ''; - let meta: Record = {}; - try { meta = JSON.parse(el.getAttribute(SELECT_META_ATTR) || '{}'); } catch {} + let meta: SelectMeta = {}; + try { meta = JSON.parse(el.getAttribute(SELECT_META_ATTR) || '{}'); } catch { /* malformed JSON defaults to empty object */ } const rect = el.getBoundingClientRect(); const semanticLabel = buildSemanticLabel(type, meta); @@ -123,8 +132,8 @@ export function computeDragPreview( if (seen.has(selectId)) return; seen.add(selectId); const type = el.getAttribute(SELECT_ATTR) || ''; - let meta: Record = {}; - try { meta = JSON.parse(el.getAttribute(SELECT_META_ATTR) || '{}'); } catch {} + let meta: SelectMeta = {}; + try { meta = JSON.parse(el.getAttribute(SELECT_META_ATTR) || '{}'); } catch { /* malformed JSON defaults to empty object */ } preview.push({ selectId, top: rect.top, diff --git a/frontend/src/app/components/onboardingConstants.ts b/frontend/src/app/components/onboardingConstants.ts index 7969574e..6cf9cbab 100644 --- a/frontend/src/app/components/onboardingConstants.ts +++ b/frontend/src/app/components/onboardingConstants.ts @@ -16,6 +16,4 @@ export const SUBSCRIPTION_PROVIDERS = [ { id: 'gemini-cli', name: 'Gemini', desc: 'Gemini 2.5 Pro & Flash', color: '#4285F4', preview: true }, { id: 'codex', name: 'ChatGPT', desc: 'GPT-5.4, o3, o4-mini', color: '#74AA9C', preview: true }, { id: 'github', name: 'GitHub Copilot', desc: 'Claude + GPT models', color: '#8B949E', preview: true }, -]; - -export type SubscriptionProvider = typeof SUBSCRIPTION_PROVIDERS[number]; +]; \ No newline at end of file diff --git a/frontend/src/app/components/richEditorUtils.ts b/frontend/src/app/components/richEditorUtils.ts index cae8450c..35fca309 100644 --- a/frontend/src/app/components/richEditorUtils.ts +++ b/frontend/src/app/components/richEditorUtils.ts @@ -1,5 +1,5 @@ export const SKILL_PILL_ATTR = 'data-skill-id'; -export const SKILL_COLOR = '#7B61BD'; +const SKILL_COLOR = '#7B61BD'; export interface AttachedSkill { id: string; diff --git a/frontend/src/app/components/useDomElementSelector.ts b/frontend/src/app/components/useDomElementSelector.ts index d9a4c6d3..966eb665 100644 --- a/frontend/src/app/components/useDomElementSelector.ts +++ b/frontend/src/app/components/useDomElementSelector.ts @@ -1,14 +1,14 @@ import { useEffect, useRef, useState, useCallback } from 'react'; import { useElementSelection } from './ElementSelectionContext'; import { - type OverlayState, type DragRect, type DragPreviewElement, type DomSelectorState, + type OverlayState, type DragRect, type DragPreviewElement, type DomSelectorState, type SelectMeta, EMPTY_OVERLAY, EMPTY_DRAG, DRAG_THRESHOLD, SELECT_ATTR, SELECT_ID_ATTR, SELECT_META_ATTR, findSelectableAncestor, buildSemanticLabel, buildSelectedElement, computeDragPreview, processDragSelection, } from './domSelectorHelpers'; -export type { OverlayState, DragRect, DragPreviewElement, DomSelectorState } from './domSelectorHelpers'; +export type { OverlayState, DragRect, DragPreviewElement } from './domSelectorHelpers'; export function useDomElementSelector(): DomSelectorState { const ctx = useElementSelection(); @@ -98,8 +98,8 @@ export function useDomElementSelector(): DomSelectorState { rafRef.current = requestAnimationFrame(() => { const rect = selectable.getBoundingClientRect(); const type = selectable.getAttribute(SELECT_ATTR) || ''; - let meta: Record = {}; - try { meta = JSON.parse(selectable.getAttribute(SELECT_META_ATTR) || '{}'); } catch {} + let meta: SelectMeta = {}; + try { meta = JSON.parse(selectable.getAttribute(SELECT_META_ATTR) || '{}'); } catch { /* malformed meta */ } const label = buildSemanticLabel(type, meta); setOverlay({ visible: true, @@ -177,17 +177,7 @@ export function useDomElementSelector(): DomSelectorState { }, [ctx]); useEffect(() => { - if (!ctx?.selectMode) { - setOverlay(EMPTY_OVERLAY); - setDragRect(EMPTY_DRAG); - setDragPreview([]); - hoveredRef.current = null; - dragOriginRef.current = null; - dragBoundsRef.current = null; - isDraggingRef.current = false; - preDragFocusRef.current = null; - return; - } + if (!ctx?.selectMode) return; const prevUserSelect = document.body.style.userSelect; document.body.style.userSelect = 'none'; diff --git a/frontend/src/app/pages/AgentChat/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput.tsx index 6ec34339..50d60106 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput.tsx @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, forwardRef, useImperativeHandle } from 'react'; +import React, { useState, useRef, useEffect, useId, forwardRef, useImperativeHandle } from 'react'; import Box from '@mui/material/Box'; import Typography from '@mui/material/Typography'; import CircularProgress from '@mui/material/CircularProgress'; @@ -16,11 +16,7 @@ import AttachmentChips from './AttachmentChips'; import ModelModeSelector from './ModelModeSelector'; import { useChatSubmit } from './hooks/useChatSubmit'; -export type { AttachedImage } from './ImageAttachments'; -export type { ForcedToolGroup } from './AttachmentChips'; -export type { AttachedSkill } from '@/app/components/richEditorUtils'; - -export interface Props { +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; @@ -48,15 +44,15 @@ const ChatInput = forwardRef(({ const generalFileInputRef = useRef(null); const elementSelection = useElementSelection(); - const fallbackOwnerIdRef = useRef(`input-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`); - const ownerId = sessionId || fallbackOwnerIdRef.current; + const fallbackOwnerId = useId(); + const ownerId = sessionId || fallbackOwnerId; useEffect(() => { if (autoFocus) editorRef.current?.focus(); }, [autoFocus]); const [hasContent, setHasContent] = useState(false); const [attachedSkills, setAttachedSkills] = useState>({}); const attachedSkillsRef = useRef(attachedSkills); - attachedSkillsRef.current = attachedSkills; + useEffect(() => { attachedSkillsRef.current = attachedSkills; }); const [picker, setPicker] = useState(EMPTY_TRIGGER); const skills = useAppSelector((state) => state.skills.items); const modesMap = useAppSelector((state) => state.modes.items);