diff --git a/client/src/components/tools/consensus/ConsensusConfig.tsx b/client/src/components/tools/consensus/ConsensusConfig.tsx index c88f861..0e9c550 100644 --- a/client/src/components/tools/consensus/ConsensusConfig.tsx +++ b/client/src/components/tools/consensus/ConsensusConfig.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Check } from 'lucide-react'; import { useConfigStore } from '@/store/config-store'; import type { ToolConfigProps } from '@/lib/types'; @@ -11,9 +11,30 @@ interface ModelConfig { export function ConsensusConfig({ tool, isOpen, onClose }: ToolConfigProps) { const dropdownRef = useRef(null); const { availableModels, updateToolConfig } = useConfigStore(); + const [positionAbove, setPositionAbove] = useState(false); const selectedModels = (tool.config.models as ModelConfig[]) || []; + // Detect if dropdown should open above or below based on available space + useEffect(() => { + if (!isOpen || !dropdownRef.current) return; + + const checkPosition = () => { + const dropdown = dropdownRef.current; + if (!dropdown) return; + + const rect = dropdown.getBoundingClientRect(); + const viewportHeight = window.innerHeight; + const spaceBelow = viewportHeight - rect.top; + const spaceAbove = rect.top; + + // If less than 400px space below and more space above, position above + setPositionAbove(spaceBelow < 400 && spaceAbove > spaceBelow); + }; + + checkPosition(); + }, [isOpen]); + useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( @@ -62,7 +83,9 @@ export function ConsensusConfig({ tool, isOpen, onClose }: ToolConfigProps) { return (

diff --git a/client/src/hooks/useChatSession.ts b/client/src/hooks/useChatSession.ts index 165223b..fc0594c 100644 --- a/client/src/hooks/useChatSession.ts +++ b/client/src/hooks/useChatSession.ts @@ -24,12 +24,12 @@ export function useChatSession() { const { uploadFilesToChat, isUploading: isUploadingFiles } = useFileUpload(); const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; - const workflowTool = tools.find((tool) => tool.enabled && tool.endpoint); + const workflowTool = tools.find((tool) => tool.enabled && tool.isWorkflow); - // Aggregate Context from Client-Side Tools - // Find all enabled tools that do NOT have an endpoint but include additional context + // Aggregate Context from Tools + // Any enabled tool can provide system context via config.prompt const systemContext = tools - .filter((t) => t.enabled && !t.endpoint && t.config?.prompt) + .filter((t) => t.enabled && t.config?.prompt) .map((t) => t.config.prompt) .join('\n\n'); diff --git a/client/src/lib/types.ts b/client/src/lib/types.ts index d112582..48a3b9d 100644 --- a/client/src/lib/types.ts +++ b/client/src/lib/types.ts @@ -10,7 +10,7 @@ export interface Tool { label: string; enabled: boolean; config: Record; - endpoint?: string; // Relative path + isWorkflow?: boolean; } // Icon type (lucide-react icon names) diff --git a/client/src/store/config-store.ts b/client/src/store/config-store.ts index 0f98819..f2e2d15 100644 --- a/client/src/store/config-store.ts +++ b/client/src/store/config-store.ts @@ -45,8 +45,8 @@ export const useConfigStore = create((set, get) => ({ }, toggleTool: async (id, enabled) => { - // Client-side tools (no endpoint) can be enabled alongside workflow tools - // Workflow tools (with endpoints) are mutually exclusive + // Workflow tools are mutually exclusive (only one can be enabled at a time) + // Any tool can provide system context via config.prompt const targetTool = get().tools.find((t) => t.id === id); const tools = get().tools.map((tool) => { @@ -54,7 +54,7 @@ export const useConfigStore = create((set, get) => ({ return { ...tool, enabled }; } // If enabling a workflow tool, disable other workflow tools - if (enabled && targetTool?.endpoint && tool.endpoint) { + if (enabled && targetTool?.isWorkflow && tool.isWorkflow) { return { ...tool, enabled: false }; } return tool; @@ -143,7 +143,7 @@ export const useConfigStore = create((set, get) => ({ try { const { currentProvider, currentModel } = get(); const systemContext = get() - .tools.filter((t) => t.enabled && !t.endpoint && t.config?.prompt) + .tools.filter((t) => t.enabled && t.config?.prompt) .map((t) => t.config.prompt) .join('\n\n'); diff --git a/server/src/db/core.ts b/server/src/db/core.ts index e1bdbeb..c67ba9d 100644 --- a/server/src/db/core.ts +++ b/server/src/db/core.ts @@ -44,7 +44,7 @@ const defaultData: Database = { { provider: 'google', model: 'gemini-3-pro-preview' }, ], }, - endpoint: 'workflow/consensus', + isWorkflow: true, }, ], chats: [], diff --git a/server/src/index.ts b/server/src/index.ts index 66d22b1..f2b63b5 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -20,7 +20,6 @@ import { updateTools, } from './db/index.js'; import { getWorkflowHandler } from './lib/workflow-router.js'; -import { handleConsensusChat } from './workflows/consensus.js'; import './workflows/consensus.js'; // Import to trigger workflow registration import chatsRouter from './routes/chats.js'; import uploadsRouter from './routes/uploads.js'; @@ -185,9 +184,6 @@ app.post('/api/chat', async (req, res) => { } }); -// Consensus workflow endpoint -app.post('/api/workflow/consensus', handleConsensusChat); - // Only start server if not imported by tests if (import.meta.url === `file://${process.argv[1]}`) { app.listen(config.port, () => { diff --git a/server/src/types.ts b/server/src/types.ts index 49cbbc5..4887d63 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -19,7 +19,7 @@ export interface Tool { label: string; enabled: boolean; config: Record; - endpoint?: string; // Relative path from API base (e.g., 'workflow/consensus') + isWorkflow?: boolean; // Tool calls a server-side workflow } export interface Attachment { diff --git a/server/src/workflows/consensus.ts b/server/src/workflows/consensus.ts index 1082727..12a0d26 100644 --- a/server/src/workflows/consensus.ts +++ b/server/src/workflows/consensus.ts @@ -4,8 +4,7 @@ import { registerWorkflow, type WorkflowHandler, } from '../lib/workflow-router.js'; -import type { Message, ChatRequest } from '../types.js'; -import type { Request, Response } from 'express'; +import type { Message } from '../types.js'; interface ModelConfig { provider: string; @@ -155,39 +154,5 @@ Provide your synthesized response now.`; return result; } -/** - * Handle consensus chat requests (backward compatibility wrapper): - * 1. Query multiple models in parallel - * 2. Synthesize responses using one of the models - * 3. Stream synthesized response back to client - */ -export async function handleConsensusChat(req: Request, res: Response) { - try { - const { messages, toolConfig, systemContext, provider, model } = - req.body as ChatRequest; - - // Use the workflow executor - const result = await executeConsensusWorkflow({ - messages, - toolConfig: toolConfig || {}, - systemContext, - provider, - model, - }); - - // Use Vercel Data Stream Protocol (compatible with useChat hook) - result.pipeDataStreamToResponse(res); - } catch (error) { - console.error('Consensus chat error:', error); - - if (!res.headersSent) { - res.status(500).json({ - error: - error instanceof Error ? error.message : 'Unknown error occurred', - }); - } - } -} - // Register the workflow registerWorkflow('consensus', executeConsensusWorkflow);