codeeditor mvp phase 1 done

This commit is contained in:
ValueOn AG 2026-02-23 18:35:42 +01:00
parent 9a59cdac86
commit 5d4ef43d5f
11 changed files with 904 additions and 1 deletions

View file

@ -164,6 +164,9 @@ function App() {
<Route path="templates" element={<FeatureViewPage view="templates" />} />
<Route path="logs" element={<FeatureViewPage view="logs" />} />
{/* Code Editor Feature Views */}
<Route path="editor" element={<FeatureViewPage view="editor" />} />
{/* Teams Bot Feature Views */}
<Route path="sessions" element={<FeatureViewPage view="sessions" />} />
<Route path="settings" element={<FeatureViewPage view="settings" />} />

View file

@ -97,6 +97,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
'feature.realestate': <FaBuilding />,
'feature.chatworkflow': <FaPlay />,
'feature.chatplayground': <FaPlay />,
'feature.codeeditor': <FaFileAlt />,
'feature.automation': <FaCogs />,
'feature.chatbot': <FaComments />,
'feature.teamsbot': <FaHeadset />,

View file

@ -32,6 +32,9 @@ import { PlaygroundPage, WorkflowsPage } from './workflows';
// Automation Views (reusing existing workflow pages)
import { AutomationsPage, AutomationTemplatesPage } from './workflows';
// CodeEditor Views
import { CodeEditorPage } from './views/codeeditor';
// Teamsbot Views
import { TeamsbotDashboardView } from './views/teamsbot/TeamsbotDashboardView';
import { TeamsbotSessionView } from './views/teamsbot/TeamsbotSessionView';
@ -123,6 +126,10 @@ const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
templates: AutomationTemplatesPage,
logs: () => <PlaceholderView title="Execution Logs" description="Automatisierungs-Ausführungsprotokolle" />,
},
codeeditor: {
editor: CodeEditorPage,
workflows: WorkflowsPage,
},
teamsbot: {
dashboard: TeamsbotDashboardView,
sessions: TeamsbotSessionView,

View file

@ -7,7 +7,7 @@
*/
import React from 'react';
import { FaCogs, FaComments, FaHeadset } from 'react-icons/fa';
import { FaCogs, FaComments, FaFileAlt, FaHeadset } from 'react-icons/fa';
import { useLanguage } from '../providers/language/LanguageContext';
import { useStore } from '../hooks/useStore';
import type { StoreFeature } from '../api/storeApi';
@ -16,6 +16,7 @@ import styles from './Store.module.css';
const FEATURE_ICONS: Record<string, React.ReactNode> = {
automation: <FaCogs />,
chatplayground: <FaComments />,
codeeditor: <FaFileAlt />,
teamsbot: <FaHeadset />,
};
@ -30,6 +31,11 @@ const FEATURE_DESCRIPTIONS: Record<string, Record<string, string>> = {
en: 'Test and experiment with AI chat models in an interactive environment.',
fr: 'Testez et experimentez avec des modeles de chat IA dans un environnement interactif.',
},
codeeditor: {
de: 'AI-gestuetzter Editor fuer Text-Dateien mit Cursor-artigem Chat und Diff-Preview.',
en: 'AI-powered editor for text files with Cursor-style chat and diff preview.',
fr: 'Editeur de fichiers texte assiste par IA avec chat et apercu des modifications.',
},
teamsbot: {
de: 'Integriere einen AI-Bot in deine Microsoft Teams Meetings und Channels.',
en: 'Integrate an AI bot into your Microsoft Teams meetings and channels.',

View file

@ -0,0 +1,344 @@
/* CodeEditor Feature Styles */
.container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.panels {
display: flex;
flex: 1;
overflow: hidden;
}
.filePanel,
.chatPanel,
.diffPanel {
display: flex;
flex-direction: column;
overflow: hidden;
}
.mainArea {
display: flex;
overflow: hidden;
}
.divider {
width: 6px;
cursor: col-resize;
background: var(--border-color, #e0e0e0);
flex-shrink: 0;
transition: background 0.15s;
}
.divider:hover {
background: var(--primary-color, #4a90d9);
}
.dragging {
cursor: col-resize;
user-select: none;
}
.dragging .divider {
background: var(--primary-color, #4a90d9);
}
/* File List Panel */
.fileList {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.panelHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color, #e0e0e0);
}
.panelHeader h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.selectedCount {
font-size: 12px;
color: var(--text-secondary, #666);
}
.fileItems {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
.fileItem {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
cursor: pointer;
transition: background 0.1s;
}
.fileItem:hover {
background: var(--hover-bg, #f5f5f5);
}
.fileItemSelected {
background: var(--selected-bg, #e8f0fe);
}
.fileCheckbox {
color: var(--primary-color, #4a90d9);
flex-shrink: 0;
}
.fileIcon {
color: var(--text-secondary, #666);
flex-shrink: 0;
font-size: 12px;
}
.fileName {
flex: 1;
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fileSize {
font-size: 11px;
color: var(--text-secondary, #999);
flex-shrink: 0;
}
.emptyState {
padding: 24px 16px;
text-align: center;
color: var(--text-secondary, #999);
font-size: 13px;
}
/* Chat Panel */
.messagesArea {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.inputArea {
border-top: 1px solid var(--border-color, #e0e0e0);
padding: 12px 16px;
}
.input {
width: 100%;
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 8px;
padding: 10px 12px;
font-size: 14px;
resize: none;
font-family: inherit;
outline: none;
transition: border-color 0.15s;
}
.input:focus {
border-color: var(--primary-color, #4a90d9);
}
.input:disabled {
background: var(--disabled-bg, #f5f5f5);
}
.inputActions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.fileCount {
font-size: 12px;
color: var(--text-secondary, #666);
}
.sendButton,
.stopButton {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
transition: opacity 0.15s;
}
.sendButton {
background: var(--primary-color, #4a90d9);
color: white;
}
.sendButton:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.stopButton {
background: var(--danger-color, #dc3545);
color: white;
}
/* Diff Preview Panel */
.diffPreview {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.diffItems {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.diffCard {
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 8px;
margin-bottom: 8px;
overflow: hidden;
}
.diffCard_pending {
border-color: var(--warning-color, #ffc107);
}
.diffCard_accepted {
border-color: var(--success-color, #28a745);
opacity: 0.7;
}
.diffCard_rejected {
border-color: var(--danger-color, #dc3545);
opacity: 0.5;
}
.diffCardHeader {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: var(--surface-bg, #f8f9fa);
border-bottom: 1px solid var(--border-color, #e0e0e0);
}
.diffFileName {
font-size: 13px;
font-weight: 600;
font-family: monospace;
}
.diffStatus {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
text-transform: uppercase;
font-weight: 600;
}
.diffStatus_pending {
background: var(--warning-light, #fff3cd);
color: var(--warning-dark, #856404);
}
.diffStatus_accepted {
background: var(--success-light, #d4edda);
color: var(--success-dark, #155724);
}
.diffStatus_rejected {
background: var(--danger-light, #f8d7da);
color: var(--danger-dark, #721c24);
}
.diffContent {
padding: 8px 12px;
max-height: 300px;
overflow-y: auto;
}
.diffOld,
.diffNew {
margin-bottom: 8px;
}
.diffLabel {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
color: var(--text-secondary, #666);
margin-bottom: 4px;
}
.diffOld pre,
.diffNew pre {
margin: 0;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
padding: 8px;
border-radius: 4px;
overflow-x: auto;
}
.diffOld pre {
background: var(--danger-light, #fff0f0);
}
.diffNew pre {
background: var(--success-light, #f0fff0);
}
.diffActions {
display: flex;
gap: 8px;
padding: 8px 12px;
border-top: 1px solid var(--border-color, #e0e0e0);
}
.acceptButton,
.rejectButton {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: opacity 0.15s;
}
.acceptButton {
background: var(--success-color, #28a745);
color: white;
}
.rejectButton {
background: var(--danger-color, #dc3545);
color: white;
}

View file

@ -0,0 +1,146 @@
/**
* CodeEditorPage
*
* Main page for the CodeEditor feature.
* Three-panel layout: FileList (left) | Chat (center) | DiffPreview (right)
*/
import React, { useState, useRef, useCallback } from 'react';
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
import { useCodeEditor } from './useCodeEditor';
import { FileListPanel } from './FileListPanel';
import { DiffPreviewPanel } from './DiffPreviewPanel';
import { useResizablePanels } from '../../../hooks/useResizablePanels';
import { Messages } from '../../../components/UiComponents';
import { FaPaperPlane, FaStop } from 'react-icons/fa';
import styles from './CodeEditor.module.css';
export const CodeEditorPage: React.FC = () => {
const { instance } = useCurrentInstance();
const instanceId = instance?.id || '';
const inputRef = useRef<HTMLTextAreaElement>(null);
const [inputValue, setInputValue] = useState('');
const {
messages,
selectedFileIds,
toggleFileSelection,
pendingEdits,
acceptEdit,
rejectEdit,
isProcessing,
sendMessage,
stopProcessing,
files,
} = useCodeEditor(instanceId);
const {
leftWidth: fileListWidth,
handleMouseDown: fileListMouseDown,
containerRef: outerContainerRef,
isDragging: isDraggingLeft,
} = useResizablePanels({
storageKey: 'codeeditor-filelist-width',
defaultLeftWidth: 20,
minLeftWidth: 10,
maxLeftWidth: 40,
});
const {
leftWidth: chatWidth,
handleMouseDown: chatMouseDown,
containerRef: innerContainerRef,
isDragging: isDraggingRight,
} = useResizablePanels({
storageKey: 'codeeditor-chat-width',
defaultLeftWidth: 60,
minLeftWidth: 30,
maxLeftWidth: 80,
});
const handleSubmit = useCallback(() => {
const trimmed = inputValue.trim();
if (!trimmed || isProcessing) return;
sendMessage(trimmed, selectedFileIds);
setInputValue('');
}, [inputValue, isProcessing, sendMessage, selectedFileIds]);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit();
}
}, [handleSubmit]);
return (
<div className={styles.container}>
<div
className={`${styles.panels} ${isDraggingLeft || isDraggingRight ? styles.dragging : ''}`}
ref={outerContainerRef}
>
{/* Left: File List */}
<div className={styles.filePanel} style={{ width: `${fileListWidth}%` }}>
<FileListPanel
files={files}
selectedFileIds={selectedFileIds}
onToggle={toggleFileSelection}
/>
</div>
<div className={styles.divider} onMouseDown={fileListMouseDown} />
{/* Center + Right */}
<div className={styles.mainArea} style={{ width: `${100 - fileListWidth}%` }} ref={innerContainerRef}>
{/* Center: Chat */}
<div className={styles.chatPanel} style={{ width: `${chatWidth}%` }}>
<div className={styles.messagesArea}>
<Messages messages={messages} />
</div>
<div className={styles.inputArea}>
<textarea
ref={inputRef}
className={styles.input}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Describe what you want to change..."
rows={2}
disabled={isProcessing}
/>
<div className={styles.inputActions}>
<span className={styles.fileCount}>
{selectedFileIds.length} file(s) selected
</span>
{isProcessing ? (
<button className={styles.stopButton} onClick={stopProcessing}>
<FaStop /> Stop
</button>
) : (
<button
className={styles.sendButton}
onClick={handleSubmit}
disabled={!inputValue.trim()}
>
<FaPaperPlane /> Send
</button>
)}
</div>
</div>
</div>
<div className={styles.divider} onMouseDown={chatMouseDown} />
{/* Right: Diff Preview */}
<div className={styles.diffPanel} style={{ width: `${100 - chatWidth}%` }}>
<DiffPreviewPanel
edits={pendingEdits}
onAccept={acceptEdit}
onReject={rejectEdit}
/>
</div>
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,95 @@
/**
* DiffPreviewPanel
*
* Shows file edit proposals as side-by-side text diffs.
* Each edit has Accept/Reject buttons.
*/
import React from 'react';
import { FaCheck, FaTimes } from 'react-icons/fa';
import styles from './CodeEditor.module.css';
export interface FileEditProposal {
id: string;
fileId: string;
fileName: string;
oldContent: string | null;
newContent: string;
status: 'pending' | 'accepted' | 'rejected';
}
interface DiffPreviewPanelProps {
edits: FileEditProposal[];
onAccept: (editId: string) => void;
onReject: (editId: string) => void;
}
export const DiffPreviewPanel: React.FC<DiffPreviewPanelProps> = ({ edits, onAccept, onReject }) => {
const pendingEdits = edits.filter(e => e.status === 'pending');
const resolvedEdits = edits.filter(e => e.status !== 'pending');
return (
<div className={styles.diffPreview}>
<div className={styles.panelHeader}>
<h3>Changes ({pendingEdits.length} pending)</h3>
</div>
<div className={styles.diffItems}>
{edits.length === 0 ? (
<div className={styles.emptyState}>No changes proposed yet</div>
) : (
<>
{pendingEdits.map((edit) => (
<DiffCard key={edit.id} edit={edit} onAccept={onAccept} onReject={onReject} />
))}
{resolvedEdits.map((edit) => (
<DiffCard key={edit.id} edit={edit} onAccept={onAccept} onReject={onReject} />
))}
</>
)}
</div>
</div>
);
};
const DiffCard: React.FC<{
edit: FileEditProposal;
onAccept: (id: string) => void;
onReject: (id: string) => void;
}> = ({ edit, onAccept, onReject }) => {
const isPending = edit.status === 'pending';
return (
<div className={`${styles.diffCard} ${styles[`diffCard_${edit.status}`]}`}>
<div className={styles.diffCardHeader}>
<span className={styles.diffFileName}>{edit.fileName}</span>
<span className={`${styles.diffStatus} ${styles[`diffStatus_${edit.status}`]}`}>
{edit.status}
</span>
</div>
<div className={styles.diffContent}>
{edit.oldContent && (
<div className={styles.diffOld}>
<div className={styles.diffLabel}>Old</div>
<pre>{edit.oldContent}</pre>
</div>
)}
<div className={styles.diffNew}>
<div className={styles.diffLabel}>New</div>
<pre>{edit.newContent}</pre>
</div>
</div>
{isPending && (
<div className={styles.diffActions}>
<button className={styles.acceptButton} onClick={() => onAccept(edit.id)}>
<FaCheck /> Accept
</button>
<button className={styles.rejectButton} onClick={() => onReject(edit.id)}>
<FaTimes /> Reject
</button>
</div>
)}
</div>
);
};

View file

@ -0,0 +1,63 @@
/**
* FileListPanel
*
* Lists uploaded text files with checkbox selection.
* Selected files are sent as context to the AI.
*/
import React from 'react';
import { FaFile, FaCheckSquare, FaRegSquare } from 'react-icons/fa';
import styles from './CodeEditor.module.css';
export interface FileInfo {
fileId: string;
fileName: string;
mimeType: string;
sizeBytes: number;
}
interface FileListPanelProps {
files: FileInfo[];
selectedFileIds: string[];
onToggle: (fileId: string) => void;
}
export const FileListPanel: React.FC<FileListPanelProps> = ({ files, selectedFileIds, onToggle }) => {
return (
<div className={styles.fileList}>
<div className={styles.panelHeader}>
<h3>Files ({files.length})</h3>
<span className={styles.selectedCount}>{selectedFileIds.length} selected</span>
</div>
<div className={styles.fileItems}>
{files.length === 0 ? (
<div className={styles.emptyState}>No text files uploaded yet</div>
) : (
files.map((file) => {
const isSelected = selectedFileIds.includes(file.fileId);
return (
<div
key={file.fileId}
className={`${styles.fileItem} ${isSelected ? styles.fileItemSelected : ''}`}
onClick={() => onToggle(file.fileId)}
>
<span className={styles.fileCheckbox}>
{isSelected ? <FaCheckSquare /> : <FaRegSquare />}
</span>
<FaFile className={styles.fileIcon} />
<span className={styles.fileName}>{file.fileName}</span>
<span className={styles.fileSize}>{_formatSize(file.sizeBytes)}</span>
</div>
);
})
)}
</div>
</div>
);
};
function _formatSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}

View file

@ -0,0 +1 @@
export { CodeEditorPage } from './CodeEditorPage';

View file

@ -0,0 +1,228 @@
/**
* useCodeEditor Hook
*
* Manages SSE connection, file selection, message state, and edit proposals
* for the CodeEditor feature.
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import api from '../../../api';
import { addCSRFTokenToHeaders, getCSRFToken, generateAndStoreCSRFToken } from '../../../utils/csrfUtils';
import type { Message } from '../../../components/UiComponents/Messages/MessagesTypes';
import type { FileInfo } from './FileListPanel';
import type { FileEditProposal } from './DiffPreviewPanel';
interface UseCodeEditorReturn {
messages: Message[];
selectedFileIds: string[];
toggleFileSelection: (fileId: string) => void;
pendingEdits: FileEditProposal[];
acceptEdit: (editId: string) => void;
rejectEdit: (editId: string) => void;
isProcessing: boolean;
sendMessage: (prompt: string, fileIds: string[]) => void;
stopProcessing: () => void;
files: FileInfo[];
}
export function useCodeEditor(instanceId: string): UseCodeEditorReturn {
const [messages, setMessages] = useState<Message[]>([]);
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([]);
const [pendingEdits, setPendingEdits] = useState<FileEditProposal[]>([]);
const [isProcessing, setIsProcessing] = useState(false);
const [files, setFiles] = useState<FileInfo[]>([]);
const [workflowId, setWorkflowId] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
if (!instanceId) return;
_loadFiles(instanceId, setFiles);
}, [instanceId]);
const toggleFileSelection = useCallback((fileId: string) => {
setSelectedFileIds(prev =>
prev.includes(fileId) ? prev.filter(id => id !== fileId) : [...prev, fileId]
);
}, []);
const sendMessage = useCallback((prompt: string, fileIds: string[]) => {
if (!instanceId || isProcessing) return;
setIsProcessing(true);
setMessages(prev => [...prev, {
id: `user-${Date.now()}`,
workflowId: workflowId || '',
role: 'user',
message: prompt,
publishedAt: Date.now() / 1000,
}]);
if (abortRef.current) {
abortRef.current.abort();
}
abortRef.current = new AbortController();
const params = new URLSearchParams();
if (workflowId) params.set('workflowId', workflowId);
const baseURL = api.defaults.baseURL || '';
const url = `${baseURL}/api/codeeditor/${instanceId}/start/stream?${params.toString()}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
const authToken = localStorage.getItem('authToken');
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
if (!getCSRFToken()) {
generateAndStoreCSRFToken();
}
addCSRFTokenToHeaders(headers);
fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({
prompt: prompt,
listFileId: fileIds,
}),
credentials: 'include',
signal: abortRef.current.signal,
}).then(async (response) => {
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
if (!response.body) {
throw new Error('Response body is null');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6);
try {
const event = JSON.parse(jsonStr);
_handleSseEvent(event, setMessages, setPendingEdits, setWorkflowId);
if (event.type === 'complete' || event.type === 'error' || event.type === 'stopped') {
setIsProcessing(false);
}
} catch {
// skip unparseable lines
}
}
}
}
setIsProcessing(false);
}).catch((err) => {
if (err.name === 'AbortError') return;
console.error('CodeEditor SSE error:', err);
setMessages(prev => [...prev, {
id: `error-${Date.now()}`,
workflowId: '',
role: 'system',
message: `Connection error: ${err.message}`,
publishedAt: Date.now() / 1000,
}]);
setIsProcessing(false);
});
}, [instanceId, isProcessing, workflowId]);
const stopProcessing = useCallback(() => {
if (abortRef.current) {
abortRef.current.abort();
}
if (!instanceId || !workflowId) return;
api.post(`/api/codeeditor/${instanceId}/${workflowId}/stop`).catch(console.error);
setIsProcessing(false);
}, [instanceId, workflowId]);
const acceptEdit = useCallback((editId: string) => {
const edit = pendingEdits.find(e => e.id === editId);
if (!edit || !instanceId || !workflowId) return;
api.post(`/api/codeeditor/${instanceId}/${workflowId}/apply`, {
fileId: edit.fileId,
fileName: edit.fileName,
newContent: edit.newContent,
}).then(() => {
setPendingEdits(prev => prev.map(e =>
e.id === editId ? { ...e, status: 'accepted' as const } : e
));
_loadFiles(instanceId, setFiles);
}).catch(console.error);
}, [pendingEdits, instanceId, workflowId]);
const rejectEdit = useCallback((editId: string) => {
setPendingEdits(prev => prev.map(e =>
e.id === editId ? { ...e, status: 'rejected' as const } : e
));
}, []);
return {
messages,
selectedFileIds,
toggleFileSelection,
pendingEdits,
acceptEdit,
rejectEdit,
isProcessing,
sendMessage,
stopProcessing,
files,
};
}
function _loadFiles(instanceId: string, setFiles: React.Dispatch<React.SetStateAction<FileInfo[]>>) {
api.get(`/api/codeeditor/${instanceId}/files`)
.then(res => setFiles(res.data.files || []))
.catch(err => console.error('Failed to load files:', err));
}
function _handleSseEvent(
event: any,
setMessages: React.Dispatch<React.SetStateAction<Message[]>>,
setPendingEdits: React.Dispatch<React.SetStateAction<FileEditProposal[]>>,
setWorkflowId: React.Dispatch<React.SetStateAction<string | null>>
) {
if (event.type === 'message' && event.item) {
const item = event.item;
setMessages(prev => [...prev, {
id: item.id || `msg-${Date.now()}-${Math.random()}`,
workflowId: item.workflowId || '',
role: item.role || 'assistant',
message: item.content || '',
publishedAt: item.createdAt || Date.now() / 1000,
documents: item.documents,
}]);
} else if (event.type === 'file_edit_proposal' && event.item) {
setPendingEdits(prev => [...prev, event.item]);
} else if (event.type === 'status') {
setMessages(prev => {
const lastIsStatus = prev.length > 0 && prev[prev.length - 1].role === 'status';
const statusMsg: Message = {
id: `status-${Date.now()}`,
workflowId: '',
role: 'status',
message: event.label || '',
publishedAt: Date.now() / 1000,
};
return lastIsStatus ? [...prev.slice(0, -1), statusMsg] : [...prev, statusMsg];
});
} else if (event.type === 'complete' && event.workflowId) {
setWorkflowId(event.workflowId);
}
}

View file

@ -250,6 +250,15 @@ export const FEATURE_REGISTRY: Record<string, FeatureConfig> = {
{ code: 'workflows', label: { de: 'Workflows', en: 'Workflows' }, path: 'workflows' },
]
},
codeeditor: {
code: 'codeeditor',
label: { de: 'Code Editor', en: 'Code Editor' },
icon: 'description',
views: [
{ code: 'editor', label: { de: 'Editor', en: 'Editor' }, path: 'editor' },
{ code: 'workflows', label: { de: 'Workflows', en: 'Workflows' }, path: 'workflows' },
]
},
teamsbot: {
code: 'teamsbot',
label: { de: 'Teams Bot', en: 'Teams Bot' },