mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[hAIk]: remove outputsSlice; migrate all 19 consumer files from state.outputs to state.apps; rewrite useViewWorkspace to use bridge thunks; remove all auto-run support
This commit is contained in:
@@ -9,7 +9,7 @@ import SystemUpdateAltIcon from '@mui/icons-material/SystemUpdateAlt';
|
||||
import Settings from '@/app/pages/Settings/Settings';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { LIST_DASHBOARDS } from '@/shared/backend-bridge/apps/dashboards';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useUpdateNotification } from './hooks/useUpdateNotification';
|
||||
import { useSidebarResize } from './hooks/useSidebarResize';
|
||||
@@ -41,7 +41,7 @@ const AppShell: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(LIST_DASHBOARDS());
|
||||
dispatch(fetchOutputs());
|
||||
dispatch(LIST_APPS());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -47,7 +47,7 @@ const Sidebar: React.FC<SidebarProps> = ({ showUpdateDot }) => {
|
||||
const dashboardList = Object.values(dashboardItems).sort(
|
||||
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(),
|
||||
);
|
||||
const appsList = Object.values(useAppSelector((s) => s.outputs.items)).sort(
|
||||
const appsList = Object.values(useAppSelector((s) => s.apps.items)).sort(
|
||||
(a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(),
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import BuildOutlinedIcon from '@mui/icons-material/BuildOutlined';
|
||||
import ViewQuiltOutlinedIcon from '@mui/icons-material/ViewQuiltOutlined';
|
||||
import { useAppSelector, useAppDispatch } from '@/shared/hooks';
|
||||
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { CommandPickerItem, MODE_ICON_MAP } from './commandPickerTypes';
|
||||
import { getToolGroupIcon } from './CommandPickerIcons';
|
||||
|
||||
@@ -17,16 +17,16 @@ export function useCommandPickerItems(trigger: '/' | '@', filter: string) {
|
||||
const modesMap = useAppSelector((s) => s.modes.items);
|
||||
const builtinTools = useAppSelector((s) => s.tools.builtinTools);
|
||||
const customTools = useAppSelector((s) => s.tools.items);
|
||||
const outputItems = useAppSelector((s) => s.outputs.items);
|
||||
const outputItems = useAppSelector((s) => s.apps.items);
|
||||
|
||||
const toolsLoaded = useAppSelector((s) => s.tools.loaded);
|
||||
const builtinLoaded = useAppSelector((s) => s.tools.builtinLoaded);
|
||||
const outputsLoaded = useAppSelector((s) => s.outputs.loaded);
|
||||
const outputsLoaded = useAppSelector((s) => s.apps.loaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (!builtinLoaded) dispatch(fetchBuiltinTools());
|
||||
if (!toolsLoaded) dispatch(fetchTools());
|
||||
if (!outputsLoaded) dispatch(fetchOutputs());
|
||||
if (!outputsLoaded) dispatch(LIST_APPS());
|
||||
}, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded]);
|
||||
|
||||
const items: CommandPickerItem[] = useMemo(() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import Icon from '@mui/material/Icon';
|
||||
import OpenInFullIcon from '@mui/icons-material/OpenInFull';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { useAppSelector } from '@/shared/hooks';
|
||||
import { SERVE_BASE } from '@/shared/state/outputsSlice';
|
||||
import { getAppServeUrl } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import ViewPreview from '../Views/ViewPreview';
|
||||
import { StreamingPlaceholder, ViewBubbleDialog } from './ViewBubbleParts';
|
||||
@@ -25,7 +25,7 @@ const ViewBubble: React.FC<Props> = ({ toolInput, toolResult, isStreaming }) =>
|
||||
|
||||
const outputId = toolInput?.output_id;
|
||||
const inputData = toolInput?.input_data || {};
|
||||
const outputsMap = useAppSelector((state) => state.outputs.items);
|
||||
const outputsMap = useAppSelector((state) => state.apps.items);
|
||||
const output = outputId ? outputsMap[outputId] : null;
|
||||
|
||||
const parsedResult = useMemo(() => {
|
||||
@@ -40,7 +40,7 @@ const ViewBubble: React.FC<Props> = ({ toolInput, toolResult, isStreaming }) =>
|
||||
const outputColor = c.accent.primary;
|
||||
const outputIcon = output?.icon || 'view_quilt';
|
||||
const hasPreview = !!frontendCode.trim();
|
||||
const serveUrl = outputId ? `${SERVE_BASE}/${outputId}/serve/index.html` : undefined;
|
||||
const serveUrl = outputId ? getAppServeUrl(outputId) : undefined;
|
||||
const inputEntries = Object.entries(inputData);
|
||||
|
||||
if (isStreaming && !hasPreview) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
} from '@assistant-ui/core';
|
||||
import { useAppSelector, useAppDispatch } from '@/shared/hooks';
|
||||
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder';
|
||||
|
||||
export interface MentionItemMetadata {
|
||||
itemType: 'skill' | 'mode' | 'file' | 'tool-group' | 'output';
|
||||
@@ -44,16 +44,16 @@ export function useOpenSwarmMentionAdapter(): Unstable_MentionAdapter {
|
||||
const modesMap = useAppSelector((s) => s.modes.items);
|
||||
const builtinTools = useAppSelector((s) => s.tools.builtinTools);
|
||||
const customTools = useAppSelector((s) => s.tools.items);
|
||||
const outputItems = useAppSelector((s) => s.outputs.items);
|
||||
const outputItems = useAppSelector((s) => s.apps.items);
|
||||
|
||||
const toolsLoaded = useAppSelector((s) => s.tools.loaded);
|
||||
const builtinLoaded = useAppSelector((s) => s.tools.builtinLoaded);
|
||||
const outputsLoaded = useAppSelector((s) => s.outputs.loaded);
|
||||
const outputsLoaded = useAppSelector((s) => s.apps.loaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (!builtinLoaded) dispatch(fetchBuiltinTools());
|
||||
if (!toolsLoaded) dispatch(fetchTools());
|
||||
if (!outputsLoaded) dispatch(fetchOutputs());
|
||||
if (!outputsLoaded) dispatch(LIST_APPS());
|
||||
}, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded]);
|
||||
|
||||
const { categories, itemsByCategory, allItems } = useMemo(() => {
|
||||
|
||||
@@ -6,9 +6,9 @@ import ViewQuiltOutlinedIcon from '@mui/icons-material/ViewQuiltOutlined';
|
||||
import { useAppSelector, useAppDispatch } from '@/shared/hooks';
|
||||
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
|
||||
import { getToolGroupIcon } from '@/app/components/CommandPicker';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { fetchSkills } from '@/shared/state/skillsSlice';
|
||||
import { fetchModes } from '@/shared/state/modesSlice';
|
||||
import { LIST_MODES } from '@/shared/state/modesSlice';
|
||||
import { SlashCommand, AtCommand, SHORTCUTS } from '../commandsTypes';
|
||||
|
||||
export function useCommands() {
|
||||
@@ -17,20 +17,20 @@ export function useCommands() {
|
||||
const modesMap = useAppSelector((state) => state.modes.items);
|
||||
const builtinTools = useAppSelector((state) => state.tools.builtinTools);
|
||||
const customTools = useAppSelector((state) => state.tools.items);
|
||||
const outputItems = useAppSelector((state) => state.outputs.items);
|
||||
const outputItems = useAppSelector((state) => state.apps.items);
|
||||
|
||||
const skillsLoaded = useAppSelector((state) => state.skills.loaded);
|
||||
const modesLoaded = useAppSelector((state) => state.modes.loaded);
|
||||
const builtinLoaded = useAppSelector((state) => state.tools.builtinLoaded);
|
||||
const toolsLoaded = useAppSelector((state) => state.tools.loaded);
|
||||
const outputsLoaded = useAppSelector((state) => state.outputs.loaded);
|
||||
const outputsLoaded = useAppSelector((state) => state.apps.loaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (!skillsLoaded) dispatch(fetchSkills());
|
||||
if (!modesLoaded) dispatch(fetchModes());
|
||||
if (!modesLoaded) dispatch(LIST_MODES());
|
||||
if (!builtinLoaded) dispatch(fetchBuiltinTools());
|
||||
if (!toolsLoaded) dispatch(fetchTools());
|
||||
if (!outputsLoaded) dispatch(fetchOutputs());
|
||||
if (!outputsLoaded) dispatch(LIST_APPS());
|
||||
}, [dispatch, skillsLoaded, modesLoaded, builtinLoaded, toolsLoaded, outputsLoaded]);
|
||||
|
||||
const slashCommands: SlashCommand[] = useMemo(() => [
|
||||
|
||||
@@ -51,7 +51,7 @@ const DashboardInner: React.FC = () => {
|
||||
const browserHomepage = useAppSelector((s) => s.settings.data.browser_homepage);
|
||||
const expandNewChats = useAppSelector((s) => s.settings.data.expand_new_chats_in_dashboard);
|
||||
const autoRevealSubAgents = useAppSelector((s) => s.settings.data.auto_reveal_sub_agents);
|
||||
const outputs = useAppSelector((s) => s.outputs.items);
|
||||
const outputs = useAppSelector((s) => s.apps.items);
|
||||
const glowingAgentCards = useAppSelector((s) => s.dashboardLayout.glowingAgentCards);
|
||||
const glowingBrowserCards = useAppSelector((s) => s.dashboardLayout.glowingBrowserCards);
|
||||
const pendingBrowserUrl = useAppSelector((s) => s.tempState.pendingBrowserUrl);
|
||||
|
||||
@@ -9,7 +9,7 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import type { AgentSession } from '@/shared/state/agentsSlice';
|
||||
import type { CardPosition, ViewCardPosition, BrowserCardPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import type { Output } from '@/shared/state/outputsSlice';
|
||||
import type { App } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import type { CanvasActions } from './useCanvasControls';
|
||||
import { STATUS_DOT, cleanUrl, CategoryGroup, ItemRow } from './DashboardHeaderParts';
|
||||
|
||||
@@ -19,7 +19,7 @@ interface DashboardHeaderProps {
|
||||
cards: Record<string, CardPosition>;
|
||||
viewCards: Record<string, ViewCardPosition>;
|
||||
browserCards: Record<string, BrowserCardPosition>;
|
||||
outputs: Record<string, Output>;
|
||||
outputs: Record<string, App>;
|
||||
dashboardId: string | undefined;
|
||||
canvasActions: CanvasActions;
|
||||
onHighlightCard?: (cardId: string) => void;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { Output, autoRunOutput, autoRunAgentOutput, executeOutput, getBackendCode, SERVE_BASE } from '@/shared/state/outputsSlice';
|
||||
import { EXECUTE_APP, getAppServeUrl } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { removeViewCard } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
@@ -24,9 +24,6 @@ const DashboardViewCard: React.FC<ViewCardProps> = ({
|
||||
|
||||
const [inputData, setInputData] = useState<Record<string, any>>(() => getDefault(output.input_schema));
|
||||
const [backendResult, setBackendResult] = useState<Record<string, any> | null>(null);
|
||||
const [autoRunning, setAutoRunning] = useState(false);
|
||||
|
||||
const hasAutoRun = !!(output.auto_run_config?.enabled && output.auto_run_config?.prompt);
|
||||
|
||||
const {
|
||||
isDragging, localDragPos, justDraggedRef,
|
||||
@@ -48,49 +45,12 @@ const DashboardViewCard: React.FC<ViewCardProps> = ({
|
||||
previewRef.current?.reload();
|
||||
};
|
||||
|
||||
const handleAutoRun = async (e: React.MouseEvent) => {
|
||||
const handleExecute = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!output.auto_run_config?.prompt) return;
|
||||
setAutoRunning(true);
|
||||
|
||||
const config = output.auto_run_config;
|
||||
const forcedToolNames = config.forced_tools?.flatMap((ft) => ft.tools) ?? [];
|
||||
|
||||
try {
|
||||
if (forcedToolNames.length > 0) {
|
||||
await dispatch(autoRunAgentOutput({
|
||||
prompt: config.prompt,
|
||||
input_schema: output.input_schema,
|
||||
output_id: output.id,
|
||||
model: config.model,
|
||||
forced_tools: forcedToolNames,
|
||||
context_paths: config.context_paths,
|
||||
})).unwrap();
|
||||
|
||||
const execRes = await dispatch(executeOutput({
|
||||
output_id: output.id,
|
||||
input_data: inputData,
|
||||
})).unwrap();
|
||||
setInputData(execRes.input_data);
|
||||
setBackendResult(execRes.backend_result);
|
||||
} else {
|
||||
const res = await dispatch(autoRunOutput({
|
||||
prompt: config.prompt,
|
||||
input_schema: output.input_schema,
|
||||
backend_code: getBackendCode(output) ?? undefined,
|
||||
context_paths: config.context_paths,
|
||||
forced_tools: forcedToolNames.length > 0 ? forcedToolNames : undefined,
|
||||
model: config.model,
|
||||
})).unwrap();
|
||||
if (res.input_data) {
|
||||
setInputData(res.input_data);
|
||||
setBackendResult(res.backend_result);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
} finally {
|
||||
setAutoRunning(false);
|
||||
}
|
||||
const res = await dispatch(EXECUTE_APP({ app_id: output.id, input_data: inputData })).unwrap();
|
||||
setBackendResult(res.backend_result as Record<string, any> | null);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
|
||||
@@ -169,14 +129,14 @@ const DashboardViewCard: React.FC<ViewCardProps> = ({
|
||||
|
||||
<ViewCardHeader
|
||||
name={output.name}
|
||||
hasAutoRun={hasAutoRun}
|
||||
autoRunning={autoRunning}
|
||||
hasAutoRun={false}
|
||||
autoRunning={false}
|
||||
isDragging={isDragging}
|
||||
onDragPointerDown={handleDragPointerDown}
|
||||
onDragPointerMove={handleDragPointerMove}
|
||||
onDragPointerUp={handleDragPointerUp}
|
||||
onRefresh={handleRefresh}
|
||||
onAutoRun={handleAutoRun}
|
||||
onAutoRun={handleExecute}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
|
||||
@@ -186,7 +146,7 @@ const DashboardViewCard: React.FC<ViewCardProps> = ({
|
||||
)}
|
||||
<ViewPreview
|
||||
ref={previewRef}
|
||||
serveUrl={`${SERVE_BASE}/${output.id}/serve/index.html`}
|
||||
serveUrl={getAppServeUrl(output.id)}
|
||||
frontendCode={output.files?.['index.html'] ?? ''}
|
||||
inputData={inputData}
|
||||
backendResult={backendResult}
|
||||
|
||||
@@ -5,15 +5,15 @@ import InputBase from '@mui/material/InputBase';
|
||||
import Icon from '@mui/material/Icon';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import type { ClaudeTokens } from '@/shared/styles/claudeTokens';
|
||||
import type { Output } from '@/shared/state/outputsSlice';
|
||||
import type { App } from '@/shared/backend-bridge/apps/app_builder';
|
||||
|
||||
interface ViewPickerPanelProps {
|
||||
searchInputRef: React.RefObject<HTMLInputElement>;
|
||||
viewSearch: string;
|
||||
onSearchChange: (q: string) => void;
|
||||
filteredOutputs: Output[];
|
||||
outputList: Output[];
|
||||
onSelect: (output: Output) => void;
|
||||
filteredOutputs: App[];
|
||||
outputList: App[];
|
||||
onSelect: (output: App) => void;
|
||||
c: ClaudeTokens;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
addBrowserCard,
|
||||
EXPANDED_CARD_MIN_H,
|
||||
} from '@/shared/state/dashboardLayoutSlice';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { LIST_APPS } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { dashboardWs } from '@/shared/ws/WebSocketManager';
|
||||
import { initBrowserCommandHandler } from '@/shared/browserCommandHandler';
|
||||
import { clearPendingBrowserUrl, clearPendingFocusAgentId } from '@/shared/state/tempStateSlice';
|
||||
@@ -52,7 +52,7 @@ export function useDashboardInit(deps: InitDeps) {
|
||||
dispatch(GET_ALL_SESSIONS(dashboardId));
|
||||
dispatch(GET_HISTORY({}));
|
||||
dispatch(fetchLayout(dashboardId));
|
||||
dispatch(fetchOutputs());
|
||||
dispatch(LIST_APPS());
|
||||
dashboardWs.connect();
|
||||
const cleanupBrowserHandler = initBrowserCommandHandler();
|
||||
return () => { cleanupBrowserHandler(); dashboardWs.disconnect(); };
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { clearHistorySearch } from '@/shared/state/agentsSlice';
|
||||
import { GET_HISTORY } from '@/shared/backend-bridge/apps/agents';
|
||||
import type { Output } from '@/shared/state/outputsSlice';
|
||||
import type { App } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import type { Props } from './toolbarShared';
|
||||
import { TOOLBAR_OWNER_ID, HISTORY_PAGE_SIZE } from './toolbarShared';
|
||||
|
||||
@@ -28,7 +28,7 @@ export function useDashboardToolbar({
|
||||
const [historyOpen, setHistoryOpen] = useState(false);
|
||||
const [historyQuery, setHistoryQuery] = useState('');
|
||||
const shortcut = useAppSelector((s) => s.settings.data.new_agent_shortcut);
|
||||
const outputs = useAppSelector((s) => s.outputs.items);
|
||||
const outputs = useAppSelector((s) => s.apps.items);
|
||||
const historySearchState = useAppSelector((s) => s.agents.historySearch);
|
||||
|
||||
const outputList = useMemo(() => Object.values(outputs), [outputs]);
|
||||
@@ -82,7 +82,7 @@ export function useDashboardToolbar({
|
||||
}
|
||||
}, [historyOpen, viewPickerOpen, onCancel, handleCloseHistory]);
|
||||
|
||||
const handleSelectView = useCallback((output: Output) => {
|
||||
const handleSelectView = useCallback((output: App) => {
|
||||
onAddView(output.id);
|
||||
setViewPickerOpen(false);
|
||||
setViewSearch('');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Output } from '@/shared/state/outputsSlice';
|
||||
import type { App } from '@/shared/backend-bridge/apps/app_builder';
|
||||
|
||||
export type ResizeDir = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
|
||||
@@ -24,7 +24,7 @@ export const HANDLE_DEFS: { dir: ResizeDir; sx: Record<string, any> }[] = [
|
||||
];
|
||||
|
||||
export interface ViewCardProps {
|
||||
output: Output;
|
||||
output: App;
|
||||
cardX: number;
|
||||
cardY: number;
|
||||
cardWidth: number;
|
||||
|
||||
@@ -6,17 +6,17 @@ import {
|
||||
ToolDefinition, BuiltinTool,
|
||||
} from '@/shared/state/toolsSlice';
|
||||
import { searchRegistry, fetchRegistryStats, fetchServerDetail, clearDetail, McpServer } from '@/shared/state/mcpRegistrySlice';
|
||||
import { fetchOutputs, updateOutput } from '@/shared/state/outputsSlice';
|
||||
import { LIST_APPS, UPDATE_APP } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { INTEGRATIONS, Integration, CATEGORY_ORDER } from '../integrations';
|
||||
import { ToolForm, emptyForm, serverToToolForm, serverToMcpConfig, groupTools } from '../toolUtils';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
|
||||
|
||||
export function useToolsState() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { items, builtinTools, builtinPermissions, loading } = useAppSelector((s) => s.tools);
|
||||
const { servers: regServers, total: regTotal, loading: regLoading, stats: regStats, detail: regDetail, detailLoading: regDetailLoading } = useAppSelector((s) => s.mcpRegistry);
|
||||
const devMode = useAppSelector((s) => s.settings.data.dev_mode);
|
||||
const outputItems = useAppSelector((s) => s.outputs.items);
|
||||
const outputItems = useAppSelector((s) => s.apps.items);
|
||||
const outputs = useMemo(() => Object.values(outputItems), [outputItems]);
|
||||
const allTools = Object.values(items);
|
||||
|
||||
@@ -70,7 +70,7 @@ export function useToolsState() {
|
||||
const coreSectionEnabled = useMemo(() => !coreTools.every((t) => builtinPermissions[t.name] === 'deny'), [coreTools, builtinPermissions]); const deferredSectionEnabled = useMemo(() => !deferredTools.every((t) => builtinPermissions[t.name] === 'deny'), [deferredTools, builtinPermissions]);
|
||||
const viewsSectionEnabled = useMemo(() => !outputs.every((o) => o.permission === 'deny'), [outputs]); const browserSectionEnabled = useMemo(() => browserTools.length > 0 && !browserTools.every((t) => builtinPermissions[t.name] === 'deny'), [browserTools, builtinPermissions]);
|
||||
|
||||
useEffect(() => { dispatch(fetchTools()); dispatch(fetchBuiltinTools()); dispatch(fetchBuiltinPermissions()); dispatch(fetchOutputs()); }, [dispatch]);
|
||||
useEffect(() => { dispatch(fetchTools()); dispatch(fetchBuiltinTools()); dispatch(fetchBuiltinPermissions()); dispatch(LIST_APPS()); }, [dispatch]);
|
||||
|
||||
const handleIntegrationToggle = async (integration: Integration) => {
|
||||
const existing = getInstalledIntegration(integration);
|
||||
@@ -223,10 +223,10 @@ export function useToolsState() {
|
||||
const handleEditInstall = (srv: McpServer) => { setRegistryOpen(false); setEditingId(null); setForm(serverToToolForm(srv)); setDialogOpen(true); };
|
||||
|
||||
const handleSectionEnabledChange = async (tls: BuiltinTool[], enabled: boolean) => { const perms: Record<string, string> = {}; for (const t of tls) perms[t.name] = enabled ? 'always_allow' : 'deny'; await dispatch(updateBuiltinPermissions(perms)); };
|
||||
const handleViewsSectionEnabledChange = async (enabled: boolean) => { for (const out of outputs) await dispatch(updateOutput({ id: out.id, permission: enabled ? 'ask' : 'deny' })); };
|
||||
const handleViewsSectionEnabledChange = async (enabled: boolean) => { for (const out of outputs) await dispatch(UPDATE_APP({ appId: out.id, permission: enabled ? 'ask' : 'deny' })); };
|
||||
const handleBuiltinPermissionChange = async (toolName: string, policy: string) => { await dispatch(updateBuiltinPermissions({ [toolName]: policy })); };
|
||||
const handleBuiltinCategoryPermissionChange = async (toolNames: string[], policy: string) => { const perms: Record<string, string> = {}; for (const name of toolNames) perms[name] = policy; await dispatch(updateBuiltinPermissions(perms)); };
|
||||
const handleViewPermissionChange = async (viewId: string, permission: string) => { await dispatch(updateOutput({ id: viewId, permission })); };
|
||||
const handleViewPermissionChange = async (viewId: string, permission: string) => { await dispatch(UPDATE_APP({ appId: viewId, permission })); };
|
||||
const toggleCategory = (cat: string) => setCollapsedCategories((p) => ({ ...p, [cat]: !p[cat] })); const toggleBuiltinExpand = (name: string) => setExpandedBuiltin((p) => (p === name ? null : name));
|
||||
|
||||
return { items, builtinPermissions, loading, outputs, devMode, regServers, regTotal, regLoading, regStats, regDetail, regDetailLoading, allTools, tools, uninstalledIntegrations, getIntegrationForTool, coreTools, deferredTools, browserTools, browserDelegationTools, browserActionTools, groupedCore, groupedDeferred, coreSectionEnabled, deferredSectionEnabled, viewsSectionEnabled, browserSectionEnabled, dialogOpen, setDialogOpen, editingId, form, setForm, collapsedCategories, toggleCategory, expandedBuiltin, toggleBuiltinExpand, coreSectionOpen, setCoreSectionOpen, deferredSectionOpen, setDeferredSectionOpen, customSectionOpen, setCustomSectionOpen, menuAnchor, handleMenuOpen, handleMenuClose, registryOpen, setRegistryOpen, regQuery, regSort, regSource, expandedServer, snackbar, setSnackbar, mcpConfigOpen, setMcpConfigOpen, mcpConfigServer, mcpAuthType, setMcpAuthType, mcpCredentials, setMcpCredentials, mcpConfigJson, setMcpConfigJson, mcpConfigError, setMcpConfigError, expandedToolId, setExpandedToolId, discovering, integrationLoading, credDialogOpen, setCredDialogOpen, credDialogIntegration, credDialogValues, setCredDialogValues, credDialogSaving, expandedServices, setExpandedServices, expandedSchema, setExpandedSchema, viewsSectionOpen, setViewsSectionOpen, browserSectionOpen, setBrowserSectionOpen, browserCollapsed, setBrowserCollapsed, builtinSectionOpen, setBuiltinSectionOpen, handleIntegrationToggle, handleDirectConnect, handleOAuthConnect, openCredentialsDialog, handleCredentialsSave, handleDisconnectIntegration, handleDiscover, handlePermissionChange, handleGroupPermissionChange, handleBulkReadOnly, handleResetPermissions, handleSave, handleDelete, openEdit, openCreate, openRegistryBrowser, handleRegSearch, handleLoadMore, handleRegSort, handleRegSourceFilter, handleExpandServer, openMcpConfigDialog, handleMcpConfigSave, handleInstall, handleEditInstall, handleSectionEnabledChange, handleViewsSectionEnabledChange, handleBuiltinPermissionChange, handleBuiltinCategoryPermissionChange, handleViewPermissionChange };
|
||||
|
||||
@@ -3,15 +3,14 @@ import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import Icon from '@mui/material/Icon';
|
||||
import { Output } from '@/shared/state/outputsSlice';
|
||||
import type { App } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
interface Props {
|
||||
output: Output;
|
||||
output: App;
|
||||
onClick: () => void;
|
||||
onDelete: () => void;
|
||||
onRun: () => void;
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import React, { useState, useRef, useCallback, useEffect, PointerEvent as ReactPointerEvent } from 'react';
|
||||
import { Box, Typography, Button, IconButton, TextField, Tabs, Tab, Tooltip, Switch, CircularProgress } from '@mui/material';
|
||||
import { Save as SaveIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Bolt as BoltIcon, Refresh as RefreshIcon, CheckCircleOutline as CheckCircleOutlineIcon, AutoFixHigh as AutoFixHighIcon } from '@mui/icons-material';
|
||||
import { Box, Typography, Button, IconButton, TextField, Tabs, Tab, Tooltip, CircularProgress } from '@mui/material';
|
||||
import { Save as SaveIcon, PlayArrow as PlayArrowIcon, Add as AddIcon, Refresh as RefreshIcon, CheckCircleOutline as CheckCircleOutlineIcon, AutoFixHigh as AutoFixHighIcon } from '@mui/icons-material';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { Output } from '@/shared/state/outputsSlice';
|
||||
import type { App } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import AgentChat from '../AgentChat/AgentChat';
|
||||
import ChatInput from '../AgentChat/ChatInput';
|
||||
import ViewPreview, { ViewPreviewHandle } from './ViewPreview';
|
||||
import InputSchemaForm, { getDefault, getStubbed } from './InputSchemaForm';
|
||||
import CodeEditor from './CodeEditor';
|
||||
import { ElementSelectionProvider } from '@/app/components/ElementSelectionContext';
|
||||
import { FileTreeItem, getEditorLanguage } from './FileTree';
|
||||
import { AutoRunLog } from './AutoRunLog';
|
||||
import { ConsolePanel } from './ConsolePanel';
|
||||
import { useViewWorkspace } from './hooks/useViewWorkspace';
|
||||
import { useAutoRun } from './hooks/useAutoRun';
|
||||
import { useViewSave } from './hooks/useViewSave';
|
||||
|
||||
interface Props { output: Output | null; onClose: () => void; }
|
||||
interface Props { output: App | null; onClose: () => void; }
|
||||
|
||||
const TAB_PREVIEW = 0, TAB_CODE = 1, TAB_TEST_INPUT = 2, TAB_AUTO_RUN = 3, TAB_CONSOLE = 4;
|
||||
const TAB_PREVIEW = 0, TAB_CODE = 1, TAB_TEST_INPUT = 2, TAB_CONSOLE = 3;
|
||||
const SIDEBAR_MIN = 280, SIDEBAR_MAX = 800;
|
||||
|
||||
const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
|
||||
@@ -42,8 +40,7 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
|
||||
const ar = useAutoRun(output, createdIdRef, ws.files, ws.name, setActiveTab);
|
||||
const save = useViewSave({
|
||||
output, name: ws.name, description: ws.description, files: ws.files,
|
||||
testInput: ar.testInput, onClose, previewRef, getAutoRunConfig: ar.getAutoRunConfig,
|
||||
autoRunEnabled: ar.autoRunEnabled, autoRunMode: ar.autoRunMode, autoRunModel: ar.autoRunModel,
|
||||
testInput: ar.testInput, onClose, previewRef,
|
||||
createdIdRef, setCreatedId,
|
||||
});
|
||||
|
||||
@@ -86,12 +83,6 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
|
||||
sx={{ flex: 1, maxWidth: 220, '& .MuiInput-input': { fontSize: '0.9rem', fontWeight: 600, color: c.text.primary }, '& .MuiInput-underline:before': { borderColor: 'transparent' }, '& .MuiInput-underline:hover:before': { borderColor: c.border.medium } }} />
|
||||
<TextField value={ws.description} onChange={(e) => ws.setDescription(e.target.value)} placeholder="Description" variant="standard" size="small"
|
||||
sx={{ flex: 2, '& .MuiInput-input': { fontSize: '0.78rem', color: c.text.muted }, '& .MuiInput-underline:before': { borderColor: 'transparent' } }} />
|
||||
{ar.autoRunEnabled && (
|
||||
<Button variant="outlined" startIcon={ar.autoRunning ? <CircularProgress size={14} /> : <BoltIcon sx={{ fontSize: 16 }} />} onClick={ar.handleAutoRun} disabled={ar.autoRunning} size="small"
|
||||
sx={{ borderColor: '#f59e0b40', color: '#f59e0b', textTransform: 'none', fontWeight: 500, fontSize: '0.8rem', px: 1.5, '&:hover': { borderColor: '#f59e0b', bgcolor: '#f59e0b10' } }}>
|
||||
{ar.autoRunning ? 'Running…' : 'Auto Run'}
|
||||
</Button>
|
||||
)}
|
||||
{save.saveStatus === 'unsaved' && <Typography sx={{ fontSize: '0.72rem', color: c.text.ghost, fontStyle: 'italic', whiteSpace: 'nowrap' }}>Unsaved changes</Typography>}
|
||||
{save.saveStatus === 'saving' && <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}><CircularProgress size={12} sx={{ color: c.text.ghost }} /><Typography sx={{ fontSize: '0.72rem', color: c.text.ghost, whiteSpace: 'nowrap' }}>Saving…</Typography></Box>}
|
||||
{save.saveStatus === 'saved' && <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}><CheckCircleOutlineIcon sx={{ fontSize: 14, color: c.accent.primary }} /><Typography sx={{ fontSize: '0.72rem', color: c.accent.primary, whiteSpace: 'nowrap' }}>Saved</Typography></Box>}
|
||||
@@ -107,7 +98,6 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
|
||||
<Tab label="Preview" value={TAB_PREVIEW} />
|
||||
<Tab label="Code" value={TAB_CODE} />
|
||||
<Tab label="Test Input" value={TAB_TEST_INPUT} />
|
||||
<Tab label="Auto Run" value={TAB_AUTO_RUN} />
|
||||
{showConsole && <Tab label="Console" value={TAB_CONSOLE} />}
|
||||
</Tabs>
|
||||
{activeTab === TAB_PREVIEW && (<>
|
||||
@@ -176,47 +166,6 @@ const ViewEditor: React.FC<Props> = ({ output, onClose }) => {
|
||||
</Box>
|
||||
)}
|
||||
{activeTab === TAB_CONSOLE && <ConsolePanel entry={ar.consoleEntry} c={c} />}
|
||||
<Box sx={{ display: activeTab === TAB_AUTO_RUN ? 'flex' : 'none', flexDirection: 'column', height: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, px: 2, py: 1, borderBottom: `1px solid ${c.border.subtle}`, bgcolor: c.bg.secondary, flexShrink: 0 }}>
|
||||
<Switch checked={ar.autoRunEnabled} onChange={(_, v) => ar.setAutoRunEnabled(v)} size="small"
|
||||
sx={{ '& .MuiSwitch-switchBase.Mui-checked': { color: '#f59e0b' }, '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { bgcolor: '#f59e0b' } }} />
|
||||
<Typography sx={{ fontSize: '0.82rem', fontWeight: 500, color: ar.autoRunEnabled ? c.text.primary : c.text.muted }}>
|
||||
{ar.autoRunEnabled ? 'Auto Run enabled' : 'Auto Run disabled'}
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
{ar.autoRunEnabled && (
|
||||
<Button size="small" startIcon={ar.autoRunning ? <CircularProgress size={12} /> : <BoltIcon sx={{ fontSize: 14 }} />} onClick={ar.handleAutoRun} disabled={ar.autoRunning}
|
||||
sx={{ textTransform: 'none', fontSize: '0.78rem', color: '#f59e0b', fontWeight: 500, '&:hover': { bgcolor: '#f59e0b10' } }}>
|
||||
{ar.autoRunning ? 'Running…' : 'Run Now'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
{ar.autoRunEnabled ? (
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', p: 2, gap: 1.5, overflow: 'hidden' }}>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.8rem', lineHeight: 1.6, flexShrink: 0 }}>
|
||||
Describe what data to generate for this app. When triggered, an LLM will produce input data matching your schema and populate the preview.
|
||||
</Typography>
|
||||
<ChatInput ref={ar.autoRunInputRef} autoRunMode onSend={() => {}} mode={ar.autoRunMode} onModeChange={ar.setAutoRunMode} model={ar.autoRunModel} onModelChange={ar.setAutoRunModel} />
|
||||
<Button variant="contained" startIcon={save.saving ? <CircularProgress size={14} color="inherit" /> : <SaveIcon sx={{ fontSize: 16 }} />} onClick={() => save.handleSave(false)} disabled={save.saving || !ws.name.trim()} size="small"
|
||||
sx={{ alignSelf: 'flex-start', flexShrink: 0, bgcolor: c.accent.primary, textTransform: 'none', fontWeight: 500, fontSize: '0.8rem', px: 2, '&:hover': { bgcolor: c.accent.hover } }}>
|
||||
{save.saving ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
{(ar.autoRunSessionId || ar.autoRunMessages.length > 0) && (
|
||||
<AutoRunLog messages={ar.autoRunMessages} status={ar.autoRunSessionStatus} logEndRef={ar.autoRunLogEndRef} c={c} />
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 1.5 }}>
|
||||
<BoltIcon sx={{ fontSize: 40, color: c.text.ghost, opacity: 0.3 }} />
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.88rem' }}>Enable Auto Run to generate live data for this app</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.78rem', maxWidth: 360, textAlign: 'center', lineHeight: 1.5 }}>
|
||||
Configure a prompt that describes what data to generate. An LLM will produce input matching your schema and populate the preview automatically.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -7,14 +7,14 @@ import Button from '@mui/material/Button';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { Output, executeOutput, OutputExecuteResult, getFrontendCode, getBackendCode, buildServeUrl, SERVE_BASE } from '@/shared/state/outputsSlice';
|
||||
import { EXECUTE_APP, getAppServeUrl, App, AppExecuteResult } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import InputSchemaForm, { getDefault } from './InputSchemaForm';
|
||||
import ViewPreview from './ViewPreview';
|
||||
|
||||
interface Props {
|
||||
output: Output;
|
||||
output: App;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@ const ViewRunDialog: React.FC<Props> = ({ output, onClose }) => {
|
||||
|
||||
const defaultInput = useMemo(() => getDefault(output.input_schema), [output.input_schema]);
|
||||
const [inputData, setInputData] = useState<Record<string, any>>(defaultInput);
|
||||
const [result, setResult] = useState<OutputExecuteResult | null>(null);
|
||||
const [result, setResult] = useState<AppExecuteResult | null>(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
|
||||
const handleRun = async () => {
|
||||
setRunning(true);
|
||||
try {
|
||||
const res = await dispatch(
|
||||
executeOutput({ output_id: output.id, input_data: inputData })
|
||||
EXECUTE_APP({ app_id: output.id, input_data: inputData })
|
||||
).unwrap();
|
||||
setResult(res);
|
||||
} finally {
|
||||
@@ -109,15 +109,15 @@ const ViewRunDialog: React.FC<Props> = ({ output, onClose }) => {
|
||||
)}
|
||||
{result ? (
|
||||
<ViewPreview
|
||||
serveUrl={`${SERVE_BASE}/${output.id}/serve/index.html`}
|
||||
frontendCode={result.frontend_code}
|
||||
inputData={result.input_data}
|
||||
backendResult={result.backend_result}
|
||||
serveUrl={getAppServeUrl(output.id)}
|
||||
frontendCode={result.frontend_code}
|
||||
inputData={inputData}
|
||||
backendResult={result.backend_result as Record<string, any> | null}
|
||||
/>
|
||||
) : (
|
||||
<ViewPreview
|
||||
serveUrl={`${SERVE_BASE}/${output.id}/serve/index.html`}
|
||||
frontendCode={getFrontendCode(output)}
|
||||
serveUrl={getAppServeUrl(output.id)}
|
||||
frontendCode={output.files?.['index.html'] ?? ''}
|
||||
inputData={inputData}
|
||||
/>
|
||||
)}
|
||||
@@ -133,7 +133,7 @@ const ViewRunDialog: React.FC<Props> = ({ output, onClose }) => {
|
||||
disabled={running}
|
||||
sx={{ bgcolor: c.accent.primary, '&:hover': { bgcolor: c.accent.hover } }}
|
||||
>
|
||||
{running ? 'Running...' : getBackendCode(output) ? 'Execute & Preview' : 'Preview'}
|
||||
{running ? 'Running...' : output.files?.['backend.py'] ? 'Execute & Preview' : 'Preview'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -5,7 +5,7 @@ import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { fetchOutputs, deleteOutput, Output } from '@/shared/state/outputsSlice';
|
||||
import { LIST_APPS, DELETE_APP, App } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import ViewCard from './ViewCard';
|
||||
import ViewEditor from './ViewEditor';
|
||||
@@ -16,32 +16,32 @@ const Views: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { id: routeId } = useParams<{ id: string }>();
|
||||
const items = useAppSelector((state) => state.outputs.items);
|
||||
const loading = useAppSelector((state) => state.outputs.loading);
|
||||
const loaded = useAppSelector((state) => state.outputs.loaded);
|
||||
const outputs = useMemo(() => Object.values(items), [items]);
|
||||
const items = useAppSelector((state) => state.apps.items);
|
||||
const loading = useAppSelector((state) => state.apps.loading);
|
||||
const loaded = useAppSelector((state) => state.apps.loaded);
|
||||
const apps = useMemo(() => Object.values(items), [items]);
|
||||
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [editingOutput, setEditingOutput] = useState<Output | null>(null);
|
||||
const [runOutput, setRunOutput] = useState<Output | null>(null);
|
||||
const [editingApp, setEditingApp] = useState<App | null>(null);
|
||||
const [runApp, setRunApp] = useState<App | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchOutputs());
|
||||
dispatch(LIST_APPS());
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded) return;
|
||||
if (routeId === 'new') {
|
||||
setEditingOutput(null);
|
||||
setEditingApp(null);
|
||||
setEditorOpen(true);
|
||||
} else if (routeId && items[routeId]) {
|
||||
setEditingOutput(items[routeId]);
|
||||
setEditingApp(items[routeId]);
|
||||
setEditorOpen(true);
|
||||
} else if (routeId && routeId !== 'new') {
|
||||
navigate('/apps', { replace: true });
|
||||
} else if (!routeId) {
|
||||
setEditorOpen(false);
|
||||
setEditingOutput(null);
|
||||
setEditingApp(null);
|
||||
}
|
||||
}, [routeId, loaded, items, navigate]);
|
||||
|
||||
@@ -49,23 +49,23 @@ const Views: React.FC = () => {
|
||||
navigate('/apps/new');
|
||||
};
|
||||
|
||||
const handleEditView = (output: Output) => {
|
||||
navigate(`/apps/${output.id}`);
|
||||
const handleEditView = (app: App) => {
|
||||
navigate(`/apps/${app.id}`);
|
||||
};
|
||||
|
||||
const handleDeleteView = (id: string) => {
|
||||
dispatch(deleteOutput(id));
|
||||
dispatch(DELETE_APP(id));
|
||||
};
|
||||
|
||||
const handleEditorClose = () => {
|
||||
setEditorOpen(false);
|
||||
setEditingOutput(null);
|
||||
dispatch(fetchOutputs());
|
||||
setEditingApp(null);
|
||||
dispatch(LIST_APPS());
|
||||
navigate('/apps');
|
||||
};
|
||||
|
||||
if (editorOpen) {
|
||||
return <ViewEditor key={editingOutput?.id ?? 'new'} output={editingOutput} onClose={handleEditorClose} />;
|
||||
return <ViewEditor key={editingApp?.id ?? 'new'} output={editingApp} onClose={handleEditorClose} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -118,7 +118,7 @@ const Views: React.FC = () => {
|
||||
<Typography sx={{ color: c.text.muted, textAlign: 'center', py: 8 }}>
|
||||
Loading...
|
||||
</Typography>
|
||||
) : outputs.length === 0 ? (
|
||||
) : apps.length === 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
textAlign: 'center',
|
||||
@@ -141,23 +141,23 @@ const Views: React.FC = () => {
|
||||
gap: 2.5,
|
||||
}}
|
||||
>
|
||||
{outputs.map((output) => (
|
||||
{apps.map((app) => (
|
||||
<ViewCard
|
||||
key={output.id}
|
||||
output={output}
|
||||
onClick={() => handleEditView(output)}
|
||||
onDelete={() => handleDeleteView(output.id)}
|
||||
onRun={() => setRunOutput(output)}
|
||||
key={app.id}
|
||||
output={app}
|
||||
onClick={() => handleEditView(app)}
|
||||
onDelete={() => handleDeleteView(app.id)}
|
||||
onRun={() => setRunApp(app)}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{runOutput && (
|
||||
{runApp && (
|
||||
<ViewRunDialog
|
||||
output={runOutput}
|
||||
onClose={() => setRunOutput(null)}
|
||||
output={runApp}
|
||||
onClose={() => setRunApp(null)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
Output, OutputExecuteResult,
|
||||
executeOutput, autoRunOutput, autoRunAgentOutput,
|
||||
cleanupAutoRunAgent, AutoRunConfig,
|
||||
} from '@/shared/state/outputsSlice';
|
||||
import { ChatInputHandle } from '../../AgentChat/ChatInput';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { EXECUTE_APP, App, AppExecuteResult } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { getDefault } from '../InputSchemaForm';
|
||||
import type { ConsoleEntry } from '../ConsolePanel';
|
||||
|
||||
const TAB_PREVIEW = 0;
|
||||
|
||||
export function useAutoRun(
|
||||
output: Output | null,
|
||||
app: App | null,
|
||||
createdIdRef: React.MutableRefObject<string | null>,
|
||||
files: Record<string, string>,
|
||||
name: string,
|
||||
@@ -27,171 +20,29 @@ export function useAutoRun(
|
||||
|
||||
const testInputDefault = useMemo(() => getDefault(parsedSchema), [parsedSchema]);
|
||||
const [testInput, setTestInput] = useState<Record<string, any>>(testInputDefault);
|
||||
useEffect(() => { setTestInput(getDefault(parsedSchema)); }, [schemaText]);
|
||||
|
||||
const [executeResult, setExecuteResult] = useState<OutputExecuteResult | null>(null);
|
||||
const [executeResult, setExecuteResult] = useState<AppExecuteResult | null>(null);
|
||||
const [consoleEntry, setConsoleEntry] = useState<ConsoleEntry | null>(null);
|
||||
const [hasNewConsoleOutput, setHasNewConsoleOutput] = useState(false);
|
||||
|
||||
const savedAutoRun = output?.auto_run_config;
|
||||
const [autoRunEnabled, setAutoRunEnabled] = useState(savedAutoRun?.enabled ?? false);
|
||||
const [autoRunMode, setAutoRunMode] = useState(savedAutoRun?.mode ?? 'agent');
|
||||
const [autoRunModel, setAutoRunModel] = useState(savedAutoRun?.model ?? 'sonnet');
|
||||
const [autoRunning, setAutoRunning] = useState(false);
|
||||
const autoRunInputRef = useRef<ChatInputHandle>(null);
|
||||
const autoRunInitialized = useRef(false);
|
||||
|
||||
const [autoRunSessionId, setAutoRunSessionId] = useState<string | null>(null);
|
||||
const autoRunLogEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const autoRunSession = useAppSelector((state) =>
|
||||
autoRunSessionId ? state.agents.sessions[autoRunSessionId] : null
|
||||
);
|
||||
const autoRunMessages = autoRunSession?.messages ?? [];
|
||||
const autoRunSessionStatus = autoRunSession?.status ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (autoRunInitialized.current || !savedAutoRun) return;
|
||||
if (!autoRunEnabled) return;
|
||||
autoRunInitialized.current = true;
|
||||
const timer = setTimeout(() => {
|
||||
autoRunInputRef.current?.setContent(
|
||||
savedAutoRun.prompt || '',
|
||||
savedAutoRun.context_paths?.map((cp) => ({ path: cp.path, type: (cp.type as 'file' | 'directory') || 'file' })),
|
||||
savedAutoRun.forced_tools,
|
||||
);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, [savedAutoRun, autoRunEnabled]);
|
||||
|
||||
const getAutoRunConfig = (): AutoRunConfig => {
|
||||
const config = autoRunInputRef.current?.getConfig();
|
||||
return {
|
||||
enabled: autoRunEnabled,
|
||||
prompt: config?.prompt ?? '',
|
||||
context_paths: config?.contextPaths?.map((cp) => ({ path: cp.path, type: cp.type })) ?? [],
|
||||
forced_tools: (config?.forcedTools ?? []).map(({ label, tools, iconKey }) => ({ label, tools, iconKey })),
|
||||
mode: autoRunMode,
|
||||
model: autoRunModel,
|
||||
};
|
||||
};
|
||||
|
||||
const handleRunPreview = async () => {
|
||||
const eid = output?.id ?? createdIdRef.current;
|
||||
const eid = app?.id ?? createdIdRef.current;
|
||||
if (!eid) { setExecuteResult(null); return; }
|
||||
setConsoleEntry({ timestamp: Date.now(), inputData: testInput, stdout: null, stderr: null, backendResult: null, error: null, source: 'execute', running: true });
|
||||
setHasNewConsoleOutput(true);
|
||||
try {
|
||||
const res = await dispatch(executeOutput({ output_id: eid, input_data: testInput })).unwrap();
|
||||
const res = await dispatch(EXECUTE_APP({ app_id: eid, input_data: testInput })).unwrap();
|
||||
setExecuteResult(res);
|
||||
setConsoleEntry({ timestamp: Date.now(), inputData: res.input_data, stdout: res.stdout ?? null, stderr: res.stderr ?? null, backendResult: res.backend_result, error: res.error, source: 'execute' });
|
||||
setConsoleEntry({ timestamp: Date.now(), inputData: testInput, stdout: res.stdout ?? null, stderr: res.stderr ?? null, backendResult: res.backend_result as Record<string, any> | null, error: res.error, source: 'execute' });
|
||||
} catch (e: any) {
|
||||
setConsoleEntry({ timestamp: Date.now(), inputData: testInput, stdout: null, stderr: null, backendResult: null, error: e?.message || 'Execution failed', source: 'execute' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutoRun = async () => {
|
||||
const config = autoRunInputRef.current?.getConfig();
|
||||
if (!config?.prompt?.trim()) return;
|
||||
setAutoRunning(true);
|
||||
let schema: Record<string, any>;
|
||||
try { schema = JSON.parse(schemaText); } catch { schema = { type: 'object', properties: {} }; }
|
||||
const forcedToolNames = config.forcedTools.flatMap((ft) => ft.tools);
|
||||
const eid = output?.id ?? createdIdRef.current;
|
||||
if (forcedToolNames.length > 0 && eid) {
|
||||
try {
|
||||
const res = await dispatch(autoRunAgentOutput({
|
||||
prompt: config.prompt, input_schema: schema, output_id: eid, model: autoRunModel,
|
||||
forced_tools: forcedToolNames,
|
||||
context_paths: config.contextPaths.map((cp) => ({ path: cp.path, type: cp.type })),
|
||||
})).unwrap();
|
||||
setAutoRunSessionId(res.session_id);
|
||||
} catch { setAutoRunning(false); }
|
||||
} else {
|
||||
try {
|
||||
const backendCode = files['backend.py'] ?? null;
|
||||
const res = await dispatch(autoRunOutput({
|
||||
prompt: config.prompt, input_schema: schema,
|
||||
backend_code: backendCode || undefined,
|
||||
context_paths: config.contextPaths.map((cp) => ({ path: cp.path, type: cp.type })),
|
||||
forced_tools: forcedToolNames.length > 0 ? forcedToolNames : undefined,
|
||||
model: autoRunModel,
|
||||
})).unwrap();
|
||||
if (res.input_data) {
|
||||
setTestInput(res.input_data);
|
||||
setExecuteResult({
|
||||
output_id: output?.id ?? createdIdRef.current ?? '', output_name: name,
|
||||
frontend_code: files['index.html'] ?? '', input_data: res.input_data,
|
||||
backend_result: res.backend_result, stdout: res.stdout ?? null,
|
||||
stderr: res.stderr ?? null, error: res.error,
|
||||
});
|
||||
setConsoleEntry({ timestamp: Date.now(), inputData: res.input_data, stdout: res.stdout ?? null, stderr: res.stderr ?? null, backendResult: res.backend_result, error: res.error, source: 'auto-run' });
|
||||
setHasNewConsoleOutput(true);
|
||||
setActiveTab(TAB_PREVIEW);
|
||||
}
|
||||
} catch {}
|
||||
setAutoRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRunSessionId || !autoRunSessionStatus) return;
|
||||
if (autoRunSessionStatus !== 'completed' && autoRunSessionStatus !== 'error' && autoRunSessionStatus !== 'stopped') return;
|
||||
let extracted = false;
|
||||
for (const msg of autoRunMessages) {
|
||||
if (msg.role !== 'tool_call' || typeof msg.content !== 'object') continue;
|
||||
const tc = msg.content as { tool?: string; input?: Record<string, any> };
|
||||
if (tc.tool !== 'RenderOutput' || !tc.input?.input_data) continue;
|
||||
setTestInput(tc.input.input_data);
|
||||
setExecuteResult({
|
||||
output_id: output?.id ?? createdIdRef.current ?? '', output_name: name,
|
||||
frontend_code: files['index.html'] ?? '', input_data: tc.input.input_data,
|
||||
backend_result: null, stdout: null, stderr: null, error: null,
|
||||
});
|
||||
setConsoleEntry({ timestamp: Date.now(), inputData: tc.input.input_data, stdout: null, stderr: null, backendResult: null, error: null, source: 'agent' });
|
||||
setHasNewConsoleOutput(true);
|
||||
setActiveTab(TAB_PREVIEW);
|
||||
extracted = true;
|
||||
break;
|
||||
}
|
||||
if (!extracted && autoRunSessionStatus === 'error') {
|
||||
const lastSys = [...autoRunMessages].reverse().find((m) => m.role === 'system');
|
||||
if (lastSys) {
|
||||
const errMsg = typeof lastSys.content === 'string' ? lastSys.content : JSON.stringify(lastSys.content);
|
||||
setExecuteResult({
|
||||
output_id: output?.id ?? createdIdRef.current ?? '', output_name: name,
|
||||
frontend_code: files['index.html'] ?? '', input_data: {},
|
||||
backend_result: null, stdout: null, stderr: null, error: errMsg,
|
||||
});
|
||||
setConsoleEntry({ timestamp: Date.now(), inputData: {}, stdout: null, stderr: null, backendResult: null, error: errMsg, source: 'agent' });
|
||||
setHasNewConsoleOutput(true);
|
||||
}
|
||||
}
|
||||
setAutoRunning(false);
|
||||
cleanupAutoRunAgent(autoRunSessionId).catch(() => {});
|
||||
setTimeout(() => setAutoRunSessionId(null), 300);
|
||||
}, [autoRunSessionId, autoRunSessionStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
autoRunLogEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, [autoRunMessages.length]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (autoRunSessionId) cleanupAutoRunAgent(autoRunSessionId).catch(() => {});
|
||||
};
|
||||
}, [autoRunSessionId]);
|
||||
|
||||
return {
|
||||
schemaText, parsedSchema, testInput, setTestInput,
|
||||
executeResult, consoleEntry,
|
||||
hasNewConsoleOutput, setHasNewConsoleOutput,
|
||||
autoRunEnabled, setAutoRunEnabled,
|
||||
autoRunMode, setAutoRunMode,
|
||||
autoRunModel, setAutoRunModel,
|
||||
autoRunning, autoRunInputRef,
|
||||
autoRunSessionId, autoRunMessages, autoRunSessionStatus,
|
||||
autoRunLogEndRef, getAutoRunConfig,
|
||||
handleRunPreview, handleAutoRun,
|
||||
handleRunPreview,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useAppDispatch } from '@/shared/hooks';
|
||||
import { createOutput, updateOutput, Output, AutoRunConfig } from '@/shared/state/outputsSlice';
|
||||
import { CREATE_APP, UPDATE_APP, App } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { ViewPreviewHandle } from '../ViewPreview';
|
||||
import { captureViewThumbnail } from '../captureViewThumbnail';
|
||||
|
||||
interface UseViewSaveParams {
|
||||
output: Output | null;
|
||||
output: App | null;
|
||||
name: string;
|
||||
description: string;
|
||||
files: Record<string, string>;
|
||||
testInput: Record<string, any>;
|
||||
onClose: () => void;
|
||||
previewRef: React.RefObject<ViewPreviewHandle | null>;
|
||||
getAutoRunConfig: () => AutoRunConfig;
|
||||
autoRunEnabled: boolean;
|
||||
autoRunMode: string;
|
||||
autoRunModel: string;
|
||||
createdIdRef: React.MutableRefObject<string | null>;
|
||||
setCreatedId: (id: string) => void;
|
||||
}
|
||||
@@ -23,7 +19,6 @@ interface UseViewSaveParams {
|
||||
export function useViewSave(params: UseViewSaveParams) {
|
||||
const {
|
||||
output, name, description, files, testInput, onClose, previewRef,
|
||||
getAutoRunConfig, autoRunEnabled, autoRunMode, autoRunModel,
|
||||
createdIdRef, setCreatedId,
|
||||
} = params;
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -39,23 +34,22 @@ export function useViewSave(params: UseViewSaveParams) {
|
||||
const buildBody = () => {
|
||||
let schema: Record<string, any>;
|
||||
try { schema = JSON.parse(files['schema.json'] ?? '{}'); } catch { schema = { type: 'object', properties: {} }; }
|
||||
const outputFiles = { ...files };
|
||||
delete outputFiles['meta.json'];
|
||||
delete outputFiles['schema.json'];
|
||||
delete outputFiles['SKILL.md'];
|
||||
const appFiles = { ...files };
|
||||
delete appFiles['meta.json'];
|
||||
delete appFiles['schema.json'];
|
||||
delete appFiles['SKILL.md'];
|
||||
return {
|
||||
name: name || 'Untitled App',
|
||||
description,
|
||||
icon: 'view_quilt',
|
||||
input_schema: schema,
|
||||
files: outputFiles,
|
||||
auto_run_config: getAutoRunConfig(),
|
||||
files: appFiles,
|
||||
};
|
||||
};
|
||||
|
||||
const captureThumbnailAsync = (outputId: string) => {
|
||||
const captureThumbnailAsync = (appId: string) => {
|
||||
captureViewThumbnail(files['index.html'] ?? '', testInput, files)
|
||||
.then((thumbnail) => { if (thumbnail) dispatch(updateOutput({ id: outputId, thumbnail })); })
|
||||
.then((thumbnail) => { if (thumbnail) dispatch(UPDATE_APP({ appId, thumbnail })); })
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
@@ -71,10 +65,10 @@ export function useViewSave(params: UseViewSaveParams) {
|
||||
const eid = output?.id ?? createdIdRef.current;
|
||||
let savedId: string;
|
||||
if (eid) {
|
||||
await dispatch(updateOutput({ id: eid, ...body })).unwrap();
|
||||
await dispatch(UPDATE_APP({ appId: eid, ...body })).unwrap();
|
||||
savedId = eid;
|
||||
} else {
|
||||
const created = await dispatch(createOutput(body)).unwrap();
|
||||
const created = await dispatch(CREATE_APP(body)).unwrap();
|
||||
savedId = created.id;
|
||||
createdIdRef.current = savedId;
|
||||
setCreatedId(savedId);
|
||||
@@ -108,7 +102,7 @@ export function useViewSave(params: UseViewSaveParams) {
|
||||
if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current);
|
||||
autoSaveTimerRef.current = setTimeout(() => { performSaveRef.current?.(false); }, 1500);
|
||||
return () => { if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); };
|
||||
}, [files, name, description, autoRunEnabled, autoRunMode, autoRunModel]);
|
||||
}, [files, name, description]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { createDraftSession, removeDraftSession } from '@/shared/state/agentsSlice';
|
||||
import { Output, SERVE_BASE } from '@/shared/state/outputsSlice';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
import { SEED_APP, READ_APP, WRITE_APP_FILE, DELETE_APP_FILE, App, getAppServeUrl } from '@/shared/backend-bridge/apps/app_builder';
|
||||
import { ViewPreviewHandle } from '../ViewPreview';
|
||||
import { buildFileTree } from '../FileTree';
|
||||
|
||||
const WORKSPACE_API = `${API_BASE}/outputs/workspace`;
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
|
||||
export function useViewWorkspace(
|
||||
output: Output | null,
|
||||
app: App | null,
|
||||
previewRef: React.RefObject<ViewPreviewHandle | null>,
|
||||
) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [files, setFiles] = useState<Record<string, string>>(() => {
|
||||
if (!output) return {};
|
||||
const f = { ...output.files };
|
||||
if (!f['schema.json'] && output.input_schema) {
|
||||
f['schema.json'] = JSON.stringify(output.input_schema, null, 2);
|
||||
if (!app) return {};
|
||||
const f = { ...app.files };
|
||||
if (!f['schema.json'] && app.input_schema) {
|
||||
f['schema.json'] = JSON.stringify(app.input_schema, null, 2);
|
||||
}
|
||||
return f;
|
||||
});
|
||||
const [fileVersion, setFileVersion] = useState(0);
|
||||
const [name, setName] = useState(output?.name ?? '');
|
||||
const [description, setDescription] = useState(output?.description ?? '');
|
||||
const [name, setName] = useState(app?.name ?? '');
|
||||
const [description, setDescription] = useState(app?.description ?? '');
|
||||
const [activeFile, setActiveFile] = useState('index.html');
|
||||
|
||||
const [workspacePath, setWorkspacePath] = useState<string | null>(null);
|
||||
@@ -45,25 +43,22 @@ export function useViewWorkspace(
|
||||
if (draftCreated.current) return;
|
||||
draftCreated.current = true;
|
||||
(async () => {
|
||||
const seedBody: Record<string, any> = { workspace_id: stableWorkspaceId };
|
||||
if (output) {
|
||||
const seedFiles: Record<string, string> = { ...output.files };
|
||||
if (output.input_schema && !seedFiles['schema.json']) {
|
||||
seedFiles['schema.json'] = JSON.stringify(output.input_schema, null, 2);
|
||||
const seedBody: { app_id: string; files?: Record<string, string>; meta?: Record<string, unknown> } = {
|
||||
app_id: stableWorkspaceId,
|
||||
};
|
||||
if (app) {
|
||||
const seedFiles: Record<string, string> = { ...app.files };
|
||||
if (app.input_schema && !seedFiles['schema.json']) {
|
||||
seedFiles['schema.json'] = JSON.stringify(app.input_schema, null, 2);
|
||||
}
|
||||
seedBody.files = seedFiles;
|
||||
seedBody.meta = { name: output.name, description: output.description };
|
||||
seedBody.meta = { name: app.name, description: app.description };
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`${WORKSPACE_API}/seed`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(seedBody),
|
||||
});
|
||||
const data = await res.json();
|
||||
setWorkspacePath(data.path);
|
||||
const result = await dispatch(SEED_APP(seedBody)).unwrap();
|
||||
setWorkspacePath(result.path);
|
||||
const action = dispatch(createDraftSession({
|
||||
mode: 'view-builder', setActive: false, targetDirectory: data.path,
|
||||
mode: 'view-builder', setActive: false, targetDirectory: result.path,
|
||||
}));
|
||||
setInitialDraftId(action.payload.draftId);
|
||||
} catch {
|
||||
@@ -71,7 +66,7 @@ export function useViewWorkspace(
|
||||
setInitialDraftId(action.payload.draftId);
|
||||
}
|
||||
})();
|
||||
}, [dispatch, output, stableWorkspaceId]);
|
||||
}, [dispatch, app, stableWorkspaceId]);
|
||||
|
||||
const effectiveSessionId = useAppSelector((state) => {
|
||||
if (!initialDraftId) return null;
|
||||
@@ -95,22 +90,21 @@ export function useViewWorkspace(
|
||||
const pollWorkspace = useCallback(async () => {
|
||||
if (!workspaceId) return;
|
||||
try {
|
||||
const res = await fetch(`${WORKSPACE_API}/${workspaceId}`);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const data = await dispatch(READ_APP(workspaceId)).unwrap();
|
||||
const fingerprint = JSON.stringify(data);
|
||||
if (fingerprint === lastPollRef.current) return;
|
||||
lastPollRef.current = fingerprint;
|
||||
if (data.files) { setFiles(data.files); setFileVersion(v => v + 1); }
|
||||
if (data.meta) {
|
||||
if (data.meta.name && !nameSetByMeta.current) {
|
||||
const meta = data.meta as Record<string, any>;
|
||||
if (meta.name && !nameSetByMeta.current) {
|
||||
nameSetByMeta.current = true;
|
||||
setName((prev) => prev || data.meta.name);
|
||||
setName((prev) => prev || meta.name);
|
||||
}
|
||||
if (data.meta.description) setDescription((prev) => prev || data.meta.description);
|
||||
if (meta.description) setDescription((prev) => prev || meta.description);
|
||||
}
|
||||
} catch {}
|
||||
}, [workspaceId]);
|
||||
}, [workspaceId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceId) return;
|
||||
@@ -130,7 +124,7 @@ export function useViewWorkspace(
|
||||
}, [initialDraftId, dispatch]);
|
||||
|
||||
const workspaceServeUrl = workspaceId
|
||||
? `${SERVE_BASE}/workspace/${workspaceId}/serve/index.html`
|
||||
? getAppServeUrl(workspaceId)
|
||||
: undefined;
|
||||
|
||||
const filePaths = useMemo(
|
||||
@@ -147,14 +141,12 @@ export function useViewWorkspace(
|
||||
if (existing) clearTimeout(existing);
|
||||
wsPushTimers.current.set(path, setTimeout(() => {
|
||||
wsPushTimers.current.delete(path);
|
||||
fetch(`${WORKSPACE_API}/${wsId}/file/${encodeURIComponent(path)}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content }),
|
||||
}).then(() => previewRef.current?.reload()).catch(() => {});
|
||||
dispatch(WRITE_APP_FILE({ appId: wsId, filepath: path, content }))
|
||||
.then(() => previewRef.current?.reload())
|
||||
.catch(() => {});
|
||||
}, 300));
|
||||
}
|
||||
}, [previewRef]);
|
||||
}, [previewRef, dispatch]);
|
||||
|
||||
const addFile = useCallback((fileName: string) => {
|
||||
const trimmed = fileName.trim();
|
||||
@@ -162,12 +154,9 @@ export function useViewWorkspace(
|
||||
setFiles(prev => ({ ...prev, [trimmed]: '' }));
|
||||
setActiveFile(trimmed);
|
||||
if (workspaceId) {
|
||||
fetch(`${WORKSPACE_API}/${workspaceId}/file/${encodeURIComponent(trimmed)}`, {
|
||||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: '' }),
|
||||
}).catch(() => {});
|
||||
dispatch(WRITE_APP_FILE({ appId: workspaceId, filepath: trimmed, content: '' }));
|
||||
}
|
||||
}, [files, workspaceId]);
|
||||
}, [files, workspaceId, dispatch]);
|
||||
|
||||
const deleteFile = useCallback((filePath: string) => {
|
||||
setFiles(prev => { const next = { ...prev }; delete next[filePath]; return next; });
|
||||
@@ -176,11 +165,9 @@ export function useViewWorkspace(
|
||||
setActiveFile(remaining[0] ?? 'index.html');
|
||||
}
|
||||
if (workspaceId) {
|
||||
fetch(`${WORKSPACE_API}/${workspaceId}/file/${encodeURIComponent(filePath)}`, {
|
||||
method: 'DELETE',
|
||||
}).catch(() => {});
|
||||
dispatch(DELETE_APP_FILE({ appId: workspaceId, filepath: filePath }));
|
||||
}
|
||||
}, [activeFile, filePaths, workspaceId]);
|
||||
}, [activeFile, filePaths, workspaceId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => { wsPushTimers.current.forEach(t => clearTimeout(t)); };
|
||||
|
||||
@@ -3,6 +3,33 @@ import { API_BASE } from '@/shared/backend-bridge/base_routes';
|
||||
|
||||
const APP_BUILDER_API: string = `${API_BASE}/app_builder`;
|
||||
|
||||
export function getAppServeUrl(appId: string): string {
|
||||
return `${APP_BUILDER_API}/${appId}/serve/index.html`;
|
||||
}
|
||||
|
||||
export interface App {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
input_schema: Record<string, any>;
|
||||
files: Record<string, string>;
|
||||
permission: string;
|
||||
thumbnail?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface AppExecuteResult {
|
||||
app_id: string;
|
||||
app_name: string;
|
||||
frontend_code: string;
|
||||
backend_result: Record<string, unknown> | null;
|
||||
stdout: string | null;
|
||||
stderr: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File serving
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -126,13 +153,13 @@ export const DELETE_APP_FILE = createAsyncThunk(
|
||||
|
||||
|
||||
const list_apps_endpoint: string = `${APP_BUILDER_API}/list`;
|
||||
async function list_apps_function(): Promise<{ apps: Record<string, unknown>[] }> {
|
||||
async function list_apps_function(): Promise<{ apps: App[] }> {
|
||||
const res = await fetch(list_apps_endpoint, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as { apps: Record<string, unknown>[] };
|
||||
return data as { apps: App[] };
|
||||
}
|
||||
export const LIST_APPS = createAsyncThunk(
|
||||
list_apps_endpoint,
|
||||
@@ -141,13 +168,13 @@ export const LIST_APPS = createAsyncThunk(
|
||||
|
||||
|
||||
const get_app_endpoint: string = `${APP_BUILDER_API}/get`;
|
||||
async function get_app_function(appId: string): Promise<Record<string, unknown>> {
|
||||
async function get_app_function(appId: string): Promise<App> {
|
||||
const res = await fetch(`${APP_BUILDER_API}/${appId}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as Record<string, unknown>;
|
||||
return data as App;
|
||||
}
|
||||
export const GET_APP = createAsyncThunk(
|
||||
get_app_endpoint,
|
||||
@@ -160,16 +187,17 @@ async function create_app_function(body: {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
input_schema?: Record<string, any>;
|
||||
files?: Record<string, string> | null;
|
||||
thumbnail?: string | null;
|
||||
}): Promise<{ ok: boolean; app: Record<string, unknown> }> {
|
||||
}): Promise<App> {
|
||||
const res = await fetch(create_app_endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as { ok: boolean; app: Record<string, unknown> };
|
||||
return data.app as App;
|
||||
}
|
||||
export const CREATE_APP = createAsyncThunk(
|
||||
create_app_endpoint,
|
||||
@@ -183,8 +211,11 @@ async function update_app_function(payload: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
input_schema?: Record<string, any>;
|
||||
files?: Record<string, string>;
|
||||
thumbnail?: string | null;
|
||||
}): Promise<{ ok: boolean; app: Record<string, unknown> }> {
|
||||
permission?: string;
|
||||
}): Promise<App> {
|
||||
const { appId, ...updates } = payload;
|
||||
const res = await fetch(`${APP_BUILDER_API}/${appId}`, {
|
||||
method: 'PUT',
|
||||
@@ -192,7 +223,7 @@ async function update_app_function(payload: {
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as { ok: boolean; app: Record<string, unknown> };
|
||||
return data.app as App;
|
||||
}
|
||||
export const UPDATE_APP = createAsyncThunk(
|
||||
update_app_endpoint,
|
||||
@@ -201,13 +232,12 @@ export const UPDATE_APP = createAsyncThunk(
|
||||
|
||||
|
||||
const delete_app_endpoint: string = `${APP_BUILDER_API}/delete`;
|
||||
async function delete_app_function(appId: string): Promise<{ ok: boolean }> {
|
||||
const res = await fetch(`${APP_BUILDER_API}/${appId}`, {
|
||||
async function delete_app_function(appId: string): Promise<string> {
|
||||
await fetch(`${APP_BUILDER_API}/${appId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as { ok: boolean };
|
||||
return appId;
|
||||
}
|
||||
export const DELETE_APP = createAsyncThunk(
|
||||
delete_app_endpoint,
|
||||
@@ -221,30 +251,17 @@ export const DELETE_APP = createAsyncThunk(
|
||||
|
||||
|
||||
const execute_app_endpoint: string = `${APP_BUILDER_API}/execute`;
|
||||
async function execute_app_function(appId: string): Promise<{
|
||||
async function execute_app_function(payload: {
|
||||
app_id: string;
|
||||
app_name: string;
|
||||
frontend_code: string;
|
||||
backend_result: Record<string, unknown> | null;
|
||||
stdout: string | null;
|
||||
stderr: string | null;
|
||||
error: string | null;
|
||||
}> {
|
||||
input_data: Record<string, any>;
|
||||
}): Promise<AppExecuteResult> {
|
||||
const res = await fetch(execute_app_endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ app_id: appId }),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data as {
|
||||
app_id: string;
|
||||
app_name: string;
|
||||
frontend_code: string;
|
||||
backend_result: Record<string, unknown> | null;
|
||||
stdout: string | null;
|
||||
stderr: string | null;
|
||||
error: string | null;
|
||||
};
|
||||
return data as AppExecuteResult;
|
||||
}
|
||||
export const EXECUTE_APP = createAsyncThunk(
|
||||
execute_app_endpoint,
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
|
||||
const OUTPUTS_API = `${API_BASE}/outputs`;
|
||||
|
||||
export const SERVE_BASE = `${API_BASE}/outputs`;
|
||||
|
||||
|
||||
export interface AutoRunConfig {
|
||||
enabled: boolean;
|
||||
prompt: string;
|
||||
context_paths: Array<{ path: string; type: string }>;
|
||||
forced_tools: Array<{ label: string; tools: string[]; iconKey?: string }>;
|
||||
mode: string;
|
||||
model: string;
|
||||
}
|
||||
|
||||
export interface Output {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
input_schema: Record<string, any>;
|
||||
files: Record<string, string>;
|
||||
permission: string;
|
||||
auto_run_config?: AutoRunConfig | null;
|
||||
thumbnail?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export function getFrontendCode(output: Output): string {
|
||||
return output.files?.['index.html'] ?? '';
|
||||
}
|
||||
|
||||
export function getBackendCode(output: Output): string | null {
|
||||
return output.files?.['backend.py'] ?? null;
|
||||
}
|
||||
|
||||
export function buildServeUrl(
|
||||
outputId: string,
|
||||
inputData: Record<string, any> = {},
|
||||
backendResult: Record<string, any> | null = null,
|
||||
): string {
|
||||
const dataPayload = JSON.stringify({ i: inputData, r: backendResult });
|
||||
const encoded = btoa(unescape(encodeURIComponent(dataPayload)));
|
||||
return `${SERVE_BASE}/${outputId}/serve/index.html?_d=${encodeURIComponent(encoded)}`;
|
||||
}
|
||||
|
||||
export interface OutputExecuteResult {
|
||||
output_id: string;
|
||||
output_name: string;
|
||||
frontend_code: string;
|
||||
input_data: Record<string, any>;
|
||||
backend_result: Record<string, any> | null;
|
||||
stdout: string | null;
|
||||
stderr: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface OutputsState {
|
||||
items: Record<string, Output>;
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
const initialState: OutputsState = { items: {}, loading: false, loaded: false };
|
||||
|
||||
export const fetchOutputs = createAsyncThunk(
|
||||
'outputs/fetch',
|
||||
async () => {
|
||||
const res = await fetch(`${OUTPUTS_API}/list`);
|
||||
const data = await res.json();
|
||||
return data.outputs as Output[];
|
||||
},
|
||||
{ condition: (_, { getState }) => !(getState() as { outputs: OutputsState }).outputs.loading },
|
||||
);
|
||||
|
||||
export const createOutput = createAsyncThunk(
|
||||
'outputs/create',
|
||||
async (body: Omit<Output, 'id' | 'created_at' | 'updated_at' | 'permission'>) => {
|
||||
const res = await fetch(`${OUTPUTS_API}/create`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Create failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.output as Output;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateOutput = createAsyncThunk(
|
||||
'outputs/update',
|
||||
async ({ id, ...updates }: Partial<Output> & { id: string }) => {
|
||||
const res = await fetch(`${OUTPUTS_API}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Update failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data.output as Output;
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteOutput = createAsyncThunk('outputs/delete', async (id: string) => {
|
||||
await fetch(`${OUTPUTS_API}/${id}`, { method: 'DELETE' });
|
||||
return id;
|
||||
});
|
||||
|
||||
export const executeOutput = createAsyncThunk(
|
||||
'outputs/execute',
|
||||
async (body: { output_id: string; input_data: Record<string, any> }) => {
|
||||
const res = await fetch(`${OUTPUTS_API}/execute`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return (await res.json()) as OutputExecuteResult;
|
||||
}
|
||||
);
|
||||
|
||||
interface AutoRunResult {
|
||||
input_data: Record<string, any> | null;
|
||||
backend_result: Record<string, any> | null;
|
||||
stdout: string | null;
|
||||
stderr: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const autoRunOutput = createAsyncThunk(
|
||||
'outputs/autoRun',
|
||||
async (body: { prompt: string; input_schema: Record<string, any>; backend_code?: string | null; context_paths?: Array<{ path: string; type: string }>; forced_tools?: string[]; model?: string }) => {
|
||||
const res = await fetch(`${OUTPUTS_API}/auto-run`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return (await res.json()) as AutoRunResult;
|
||||
}
|
||||
);
|
||||
|
||||
interface AutoRunAgentResult {
|
||||
session_id: string;
|
||||
}
|
||||
|
||||
export const autoRunAgentOutput = createAsyncThunk(
|
||||
'outputs/autoRunAgent',
|
||||
async (body: {
|
||||
prompt: string;
|
||||
input_schema: Record<string, any>;
|
||||
output_id: string;
|
||||
model?: string;
|
||||
forced_tools?: string[];
|
||||
context_paths?: Array<{ path: string; type: string }>;
|
||||
}) => {
|
||||
const res = await fetch(`${OUTPUTS_API}/auto-run-agent`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Auto-run agent launch failed: ${res.status}`);
|
||||
return (await res.json()) as AutoRunAgentResult;
|
||||
}
|
||||
);
|
||||
|
||||
export async function cleanupAutoRunAgent(sessionId: string): Promise<void> {
|
||||
await fetch(`${OUTPUTS_API}/auto-run-agent/${sessionId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
const outputsSlice = createSlice({
|
||||
name: 'outputs',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchOutputs.pending, (state) => { state.loading = true; })
|
||||
.addCase(fetchOutputs.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.loaded = true;
|
||||
state.items = {};
|
||||
for (const o of action.payload) state.items[o.id] = o;
|
||||
})
|
||||
.addCase(fetchOutputs.rejected, (state) => { state.loading = false; state.loaded = true; })
|
||||
.addCase(createOutput.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
|
||||
.addCase(updateOutput.fulfilled, (state, action) => { state.items[action.payload.id] = action.payload; })
|
||||
.addCase(deleteOutput.fulfilled, (state, action) => { delete state.items[action.payload]; });
|
||||
},
|
||||
});
|
||||
|
||||
export default outputsSlice.reducer;
|
||||
@@ -7,7 +7,6 @@ import modesReducer from './modesSlice';
|
||||
import settingsReducer from './settingsSlice';
|
||||
import mcpRegistryReducer from './mcpRegistrySlice';
|
||||
import skillRegistryReducer from './skillRegistrySlice';
|
||||
import outputsReducer from './outputsSlice';
|
||||
import dashboardLayoutReducer from './dashboardLayoutSlice';
|
||||
import dashboardsReducer from './dashboardsSlice';
|
||||
import updateReducer from './updateSlice';
|
||||
@@ -23,7 +22,6 @@ export const store = configureStore({
|
||||
settings: settingsReducer,
|
||||
mcpRegistry: mcpRegistryReducer,
|
||||
skillRegistry: skillRegistryReducer,
|
||||
outputs: outputsReducer,
|
||||
dashboardLayout: dashboardLayoutReducer,
|
||||
dashboards: dashboardsReducer,
|
||||
update: updateReducer,
|
||||
|
||||
Reference in New Issue
Block a user