phase 2 codeeditor
This commit is contained in:
parent
5d4ef43d5f
commit
60887682a5
6 changed files with 281 additions and 13 deletions
|
|
@ -33,7 +33,7 @@ import { PlaygroundPage, WorkflowsPage } from './workflows';
|
||||||
import { AutomationsPage, AutomationTemplatesPage } from './workflows';
|
import { AutomationsPage, AutomationTemplatesPage } from './workflows';
|
||||||
|
|
||||||
// CodeEditor Views
|
// CodeEditor Views
|
||||||
import { CodeEditorPage } from './views/codeeditor';
|
import { CodeEditorPage, CodeEditorWorkflowsPage } from './views/codeeditor';
|
||||||
|
|
||||||
// Teamsbot Views
|
// Teamsbot Views
|
||||||
import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView';
|
import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView';
|
||||||
|
|
@ -128,7 +128,7 @@ const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
|
||||||
},
|
},
|
||||||
codeeditor: {
|
codeeditor: {
|
||||||
editor: CodeEditorPage,
|
editor: CodeEditorPage,
|
||||||
workflows: WorkflowsPage,
|
workflows: CodeEditorWorkflowsPage,
|
||||||
},
|
},
|
||||||
teamsbot: {
|
teamsbot: {
|
||||||
dashboard: TeamsbotDashboardView,
|
dashboard: TeamsbotDashboardView,
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,65 @@
|
||||||
background: var(--disabled-bg, #f5f5f5);
|
background: var(--disabled-bg, #f5f5f5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mode Toggle */
|
||||||
|
.modeToggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modeButton {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border: 1px solid var(--border-color, #e0e0e0);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modeButton:hover {
|
||||||
|
background: var(--hover-bg, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modeActive {
|
||||||
|
background: var(--primary-color, #4a90d9);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--primary-color, #4a90d9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modeActive:hover {
|
||||||
|
background: var(--primary-dark, #3a7bc8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modeButton:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Agent Progress */
|
||||||
|
.agentProgress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 8px 0;
|
||||||
|
background: var(--info-light, #e8f4fd);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--info-dark, #0c5460);
|
||||||
|
animation: pulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.7; }
|
||||||
|
}
|
||||||
|
|
||||||
.inputActions {
|
.inputActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -342,3 +401,65 @@
|
||||||
background: var(--danger-color, #dc3545);
|
background: var(--danger-color, #dc3545);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Workflows Page */
|
||||||
|
.workflowsPage {
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflowsHeader {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflowsHeader h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refreshButton {
|
||||||
|
padding: 6px 14px;
|
||||||
|
border: 1px solid var(--border-color, #e0e0e0);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refreshButton:hover {
|
||||||
|
background: var(--hover-bg, #f5f5f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflowTable {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflowTable th,
|
||||||
|
.workflowTable td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflowTable th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary, #666);
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusBadge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status_running { background: var(--info-light, #e8f4fd); color: var(--info-dark, #0c5460); }
|
||||||
|
.status_completed { background: var(--success-light, #d4edda); color: var(--success-dark, #155724); }
|
||||||
|
.status_stopped { background: var(--warning-light, #fff3cd); color: var(--warning-dark, #856404); }
|
||||||
|
.status_error { background: var(--danger-light, #f8d7da); color: var(--danger-dark, #721c24); }
|
||||||
|
.status_unknown { background: #f0f0f0; color: #666; }
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
*
|
*
|
||||||
* Main page for the CodeEditor feature.
|
* Main page for the CodeEditor feature.
|
||||||
* Three-panel layout: FileList (left) | Chat (center) | DiffPreview (right)
|
* Three-panel layout: FileList (left) | Chat (center) | DiffPreview (right)
|
||||||
|
* Supports simple mode (Phase 1) and agent mode (Phase 2).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useRef, useCallback } from 'react';
|
import React, { useState, useRef, useCallback } from 'react';
|
||||||
|
|
@ -12,7 +13,7 @@ import { FileListPanel } from './FileListPanel';
|
||||||
import { DiffPreviewPanel } from './DiffPreviewPanel';
|
import { DiffPreviewPanel } from './DiffPreviewPanel';
|
||||||
import { useResizablePanels } from '../../../hooks/useResizablePanels';
|
import { useResizablePanels } from '../../../hooks/useResizablePanels';
|
||||||
import { Messages } from '../../../components/UiComponents';
|
import { Messages } from '../../../components/UiComponents';
|
||||||
import { FaPaperPlane, FaStop } from 'react-icons/fa';
|
import { FaPaperPlane, FaStop, FaRobot, FaEdit } from 'react-icons/fa';
|
||||||
import styles from './CodeEditor.module.css';
|
import styles from './CodeEditor.module.css';
|
||||||
|
|
||||||
export const CodeEditorPage: React.FC = () => {
|
export const CodeEditorPage: React.FC = () => {
|
||||||
|
|
@ -20,6 +21,7 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
const instanceId = instance?.id || '';
|
const instanceId = instance?.id || '';
|
||||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [mode, setMode] = useState<'simple' | 'agent'>('simple');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
|
|
@ -32,6 +34,7 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
stopProcessing,
|
stopProcessing,
|
||||||
files,
|
files,
|
||||||
|
agentProgress,
|
||||||
} = useCodeEditor(instanceId);
|
} = useCodeEditor(instanceId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -61,9 +64,9 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSubmit = useCallback(() => {
|
||||||
const trimmed = inputValue.trim();
|
const trimmed = inputValue.trim();
|
||||||
if (!trimmed || isProcessing) return;
|
if (!trimmed || isProcessing) return;
|
||||||
sendMessage(trimmed, selectedFileIds);
|
sendMessage(trimmed, selectedFileIds, mode);
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
}, [inputValue, isProcessing, sendMessage, selectedFileIds]);
|
}, [inputValue, isProcessing, sendMessage, selectedFileIds, mode]);
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
|
@ -95,22 +98,57 @@ export const CodeEditorPage: React.FC = () => {
|
||||||
<div className={styles.chatPanel} style={{ width: `${chatWidth}%` }}>
|
<div className={styles.chatPanel} style={{ width: `${chatWidth}%` }}>
|
||||||
<div className={styles.messagesArea}>
|
<div className={styles.messagesArea}>
|
||||||
<Messages messages={messages} />
|
<Messages messages={messages} />
|
||||||
|
|
||||||
|
{agentProgress && isProcessing && (
|
||||||
|
<div className={styles.agentProgress}>
|
||||||
|
<FaRobot />
|
||||||
|
<span>
|
||||||
|
Round {agentProgress.round} | {agentProgress.totalToolCalls} tools |{' '}
|
||||||
|
{agentProgress.costCHF.toFixed(4)} CHF
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.inputArea}>
|
<div className={styles.inputArea}>
|
||||||
|
<div className={styles.modeToggle}>
|
||||||
|
<button
|
||||||
|
className={`${styles.modeButton} ${mode === 'simple' ? styles.modeActive : ''}`}
|
||||||
|
onClick={() => setMode('simple')}
|
||||||
|
disabled={isProcessing}
|
||||||
|
title="Single AI call with selected files as context"
|
||||||
|
>
|
||||||
|
<FaEdit /> Simple
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${styles.modeButton} ${mode === 'agent' ? styles.modeActive : ''}`}
|
||||||
|
onClick={() => setMode('agent')}
|
||||||
|
disabled={isProcessing}
|
||||||
|
title="AI agent with tools (reads files autonomously, multi-step)"
|
||||||
|
>
|
||||||
|
<FaRobot /> Agent
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Describe what you want to change..."
|
placeholder={mode === 'agent'
|
||||||
|
? "Describe a complex task (e.g. 'Document all Python files')..."
|
||||||
|
: "Describe what you want to change..."
|
||||||
|
}
|
||||||
rows={2}
|
rows={2}
|
||||||
disabled={isProcessing}
|
disabled={isProcessing}
|
||||||
/>
|
/>
|
||||||
<div className={styles.inputActions}>
|
<div className={styles.inputActions}>
|
||||||
<span className={styles.fileCount}>
|
<span className={styles.fileCount}>
|
||||||
{selectedFileIds.length} file(s) selected
|
{mode === 'simple'
|
||||||
|
? `${selectedFileIds.length} file(s) selected`
|
||||||
|
: `Agent mode: AI reads files autonomously`
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<button className={styles.stopButton} onClick={stopProcessing}>
|
<button className={styles.stopButton} onClick={stopProcessing}>
|
||||||
|
|
|
||||||
83
src/pages/views/codeeditor/CodeEditorWorkflowsPage.tsx
Normal file
83
src/pages/views/codeeditor/CodeEditorWorkflowsPage.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
/**
|
||||||
|
* CodeEditorWorkflowsPage
|
||||||
|
*
|
||||||
|
* Lists CodeEditor workflows for the current feature instance.
|
||||||
|
* Uses the codeeditor-specific API endpoint instead of the generic /api/workflows/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||||
|
import api from '../../../api';
|
||||||
|
import styles from './CodeEditor.module.css';
|
||||||
|
|
||||||
|
interface WorkflowItem {
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
status?: string;
|
||||||
|
workflowMode?: string;
|
||||||
|
startedAt?: number;
|
||||||
|
lastActivity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CodeEditorWorkflowsPage: React.FC = () => {
|
||||||
|
const { instance } = useCurrentInstance();
|
||||||
|
const instanceId = instance?.id || '';
|
||||||
|
const [workflows, setWorkflows] = useState<WorkflowItem[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const loadWorkflows = useCallback(() => {
|
||||||
|
if (!instanceId) return;
|
||||||
|
setLoading(true);
|
||||||
|
api.get(`/api/codeeditor/${instanceId}/workflows`)
|
||||||
|
.then(res => {
|
||||||
|
const items = res.data?.items || res.data?.workflows || [];
|
||||||
|
setWorkflows(Array.isArray(items) ? items : []);
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Failed to load workflows:', err))
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [instanceId]);
|
||||||
|
|
||||||
|
useEffect(() => { loadWorkflows(); }, [loadWorkflows]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.workflowsPage}>
|
||||||
|
<div className={styles.workflowsHeader}>
|
||||||
|
<h3>CodeEditor Workflows</h3>
|
||||||
|
<button className={styles.refreshButton} onClick={loadWorkflows} disabled={loading}>
|
||||||
|
{loading ? 'Loading...' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{workflows.length === 0 ? (
|
||||||
|
<div className={styles.emptyState}>
|
||||||
|
{loading ? 'Loading workflows...' : 'No workflows yet. Start a conversation in the Editor view.'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<table className={styles.workflowTable}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Label</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Started</th>
|
||||||
|
<th>Last Activity</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{workflows.map(wf => (
|
||||||
|
<tr key={wf.id}>
|
||||||
|
<td>{wf.label || wf.id.slice(0, 8)}</td>
|
||||||
|
<td>
|
||||||
|
<span className={`${styles.statusBadge} ${styles[`status_${wf.status || 'unknown'}`]}`}>
|
||||||
|
{wf.status || 'unknown'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{wf.startedAt ? new Date(wf.startedAt * 1000).toLocaleString() : '-'}</td>
|
||||||
|
<td>{wf.lastActivity ? new Date(wf.lastActivity * 1000).toLocaleString() : '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
export { CodeEditorPage } from './CodeEditorPage';
|
export { CodeEditorPage } from './CodeEditorPage';
|
||||||
|
export { CodeEditorWorkflowsPage } from './CodeEditorWorkflowsPage';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
/**
|
/**
|
||||||
* useCodeEditor Hook
|
* useCodeEditor Hook
|
||||||
*
|
*
|
||||||
* Manages SSE connection, file selection, message state, and edit proposals
|
* Manages SSE connection, file selection, message state, edit proposals,
|
||||||
* for the CodeEditor feature.
|
* and agent mode progress for the CodeEditor feature.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
|
|
@ -12,6 +12,13 @@ import type { Message } from '../../../components/UiComponents/Messages/Messages
|
||||||
import type { FileInfo } from './FileListPanel';
|
import type { FileInfo } from './FileListPanel';
|
||||||
import type { FileEditProposal } from './DiffPreviewPanel';
|
import type { FileEditProposal } from './DiffPreviewPanel';
|
||||||
|
|
||||||
|
export interface AgentProgress {
|
||||||
|
round: number;
|
||||||
|
totalAiCalls: number;
|
||||||
|
totalToolCalls: number;
|
||||||
|
costCHF: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface UseCodeEditorReturn {
|
interface UseCodeEditorReturn {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
selectedFileIds: string[];
|
selectedFileIds: string[];
|
||||||
|
|
@ -20,9 +27,10 @@ interface UseCodeEditorReturn {
|
||||||
acceptEdit: (editId: string) => void;
|
acceptEdit: (editId: string) => void;
|
||||||
rejectEdit: (editId: string) => void;
|
rejectEdit: (editId: string) => void;
|
||||||
isProcessing: boolean;
|
isProcessing: boolean;
|
||||||
sendMessage: (prompt: string, fileIds: string[]) => void;
|
sendMessage: (prompt: string, fileIds: string[], mode?: 'simple' | 'agent') => void;
|
||||||
stopProcessing: () => void;
|
stopProcessing: () => void;
|
||||||
files: FileInfo[];
|
files: FileInfo[];
|
||||||
|
agentProgress: AgentProgress | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
|
|
@ -32,6 +40,7 @@ export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
const [isProcessing, setIsProcessing] = useState(false);
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
const [files, setFiles] = useState<FileInfo[]>([]);
|
const [files, setFiles] = useState<FileInfo[]>([]);
|
||||||
const [workflowId, setWorkflowId] = useState<string | null>(null);
|
const [workflowId, setWorkflowId] = useState<string | null>(null);
|
||||||
|
const [agentProgress, setAgentProgress] = useState<AgentProgress | null>(null);
|
||||||
const abortRef = useRef<AbortController | null>(null);
|
const abortRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -45,10 +54,11 @@ export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const sendMessage = useCallback((prompt: string, fileIds: string[]) => {
|
const sendMessage = useCallback((prompt: string, fileIds: string[], mode: 'simple' | 'agent' = 'simple') => {
|
||||||
if (!instanceId || isProcessing) return;
|
if (!instanceId || isProcessing) return;
|
||||||
|
|
||||||
setIsProcessing(true);
|
setIsProcessing(true);
|
||||||
|
setAgentProgress(null);
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
id: `user-${Date.now()}`,
|
id: `user-${Date.now()}`,
|
||||||
workflowId: workflowId || '',
|
workflowId: workflowId || '',
|
||||||
|
|
@ -64,6 +74,7 @@ export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
if (workflowId) params.set('workflowId', workflowId);
|
if (workflowId) params.set('workflowId', workflowId);
|
||||||
|
params.set('mode', mode);
|
||||||
|
|
||||||
const baseURL = api.defaults.baseURL || '';
|
const baseURL = api.defaults.baseURL || '';
|
||||||
const url = `${baseURL}/api/codeeditor/${instanceId}/start/stream?${params.toString()}`;
|
const url = `${baseURL}/api/codeeditor/${instanceId}/start/stream?${params.toString()}`;
|
||||||
|
|
@ -115,7 +126,7 @@ export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
const jsonStr = line.slice(6);
|
const jsonStr = line.slice(6);
|
||||||
try {
|
try {
|
||||||
const event = JSON.parse(jsonStr);
|
const event = JSON.parse(jsonStr);
|
||||||
_handleSseEvent(event, setMessages, setPendingEdits, setWorkflowId);
|
_handleSseEvent(event, setMessages, setPendingEdits, setWorkflowId, setAgentProgress);
|
||||||
|
|
||||||
if (event.type === 'complete' || event.type === 'error' || event.type === 'stopped') {
|
if (event.type === 'complete' || event.type === 'error' || event.type === 'stopped') {
|
||||||
setIsProcessing(false);
|
setIsProcessing(false);
|
||||||
|
|
@ -183,6 +194,7 @@ export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
|
||||||
sendMessage,
|
sendMessage,
|
||||||
stopProcessing,
|
stopProcessing,
|
||||||
files,
|
files,
|
||||||
|
agentProgress,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,7 +208,8 @@ function _handleSseEvent(
|
||||||
event: any,
|
event: any,
|
||||||
setMessages: React.Dispatch<React.SetStateAction<Message[]>>,
|
setMessages: React.Dispatch<React.SetStateAction<Message[]>>,
|
||||||
setPendingEdits: React.Dispatch<React.SetStateAction<FileEditProposal[]>>,
|
setPendingEdits: React.Dispatch<React.SetStateAction<FileEditProposal[]>>,
|
||||||
setWorkflowId: React.Dispatch<React.SetStateAction<string | null>>
|
setWorkflowId: React.Dispatch<React.SetStateAction<string | null>>,
|
||||||
|
setAgentProgress: React.Dispatch<React.SetStateAction<AgentProgress | null>>
|
||||||
) {
|
) {
|
||||||
if (event.type === 'message' && event.item) {
|
if (event.type === 'message' && event.item) {
|
||||||
const item = event.item;
|
const item = event.item;
|
||||||
|
|
@ -222,6 +235,18 @@ function _handleSseEvent(
|
||||||
};
|
};
|
||||||
return lastIsStatus ? [...prev.slice(0, -1), statusMsg] : [...prev, statusMsg];
|
return lastIsStatus ? [...prev.slice(0, -1), statusMsg] : [...prev, statusMsg];
|
||||||
});
|
});
|
||||||
|
} else if (event.type === 'agent_progress' && event.item) {
|
||||||
|
setAgentProgress(event.item);
|
||||||
|
} else if (event.type === 'agent_summary' && event.item) {
|
||||||
|
const s = event.item;
|
||||||
|
setMessages(prev => [...prev, {
|
||||||
|
id: `summary-${Date.now()}`,
|
||||||
|
workflowId: '',
|
||||||
|
role: 'system',
|
||||||
|
message: `Agent completed: ${s.rounds} rounds, ${s.totalToolCalls} tool calls, ${s.costCHF} CHF, ${s.processingTime}s`,
|
||||||
|
publishedAt: Date.now() / 1000,
|
||||||
|
}]);
|
||||||
|
setAgentProgress(null);
|
||||||
} else if (event.type === 'complete' && event.workflowId) {
|
} else if (event.type === 'complete' && event.workflowId) {
|
||||||
setWorkflowId(event.workflowId);
|
setWorkflowId(event.workflowId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue