Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions client/src/components/tools/consensus/ConsensusConfig.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -11,9 +11,30 @@ interface ModelConfig {
export function ConsensusConfig({ tool, isOpen, onClose }: ToolConfigProps) {
const dropdownRef = useRef<HTMLDivElement>(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 (
Expand Down Expand Up @@ -62,7 +83,9 @@ export function ConsensusConfig({ tool, isOpen, onClose }: ToolConfigProps) {
return (
<div
ref={dropdownRef}
className="absolute left-0 top-full mt-2 w-72 bg-secondary border border-border rounded-lg shadow-lg max-h-96 overflow-y-auto z-50"
className={`absolute left-0 w-72 bg-secondary border border-border rounded-lg shadow-lg max-h-96 overflow-y-auto z-50 ${
positionAbove ? 'bottom-full mb-2' : 'top-full mt-2'
}`}
>
<div className="p-3 border-b border-border">
<h3 className="text-sm font-medium text-foreground">
Expand Down
8 changes: 4 additions & 4 deletions client/src/hooks/useChatSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
2 changes: 1 addition & 1 deletion client/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export interface Tool {
label: string;
enabled: boolean;
config: Record<string, unknown>;
endpoint?: string; // Relative path
isWorkflow?: boolean;
}

// Icon type (lucide-react icon names)
Expand Down
8 changes: 4 additions & 4 deletions client/src/store/config-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ export const useConfigStore = create<ConfigState>((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) => {
if (tool.id === id) {
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;
Expand Down Expand Up @@ -143,7 +143,7 @@ export const useConfigStore = create<ConfigState>((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');

Expand Down
2 changes: 1 addition & 1 deletion server/src/db/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const defaultData: Database = {
{ provider: 'google', model: 'gemini-3-pro-preview' },
],
},
endpoint: 'workflow/consensus',
isWorkflow: true,
},
],
chats: [],
Expand Down
4 changes: 0 additions & 4 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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, () => {
Expand Down
2 changes: 1 addition & 1 deletion server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface Tool {
label: string;
enabled: boolean;
config: Record<string, unknown>;
endpoint?: string; // Relative path from API base (e.g., 'workflow/consensus')
isWorkflow?: boolean; // Tool calls a server-side workflow
}

export interface Attachment {
Expand Down
37 changes: 1 addition & 36 deletions server/src/workflows/consensus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);