diff --git a/src/components/add-items/addItemRegistry.tsx b/src/components/add-items/addItemRegistry.tsx
index 3e4cb55c..89009289 100644
--- a/src/components/add-items/addItemRegistry.tsx
+++ b/src/components/add-items/addItemRegistry.tsx
@@ -1,11 +1,9 @@
import React from 'react';
import {
AppWindow,
- ArrowRightLeft,
Boxes,
Component,
GitBranch,
- Group,
Image as ImageIcon,
LayoutList,
Smartphone,
@@ -26,11 +24,9 @@ export type AddItemId =
| 'circle'
| 'sticky-note'
| 'text'
- | 'section'
| 'journey'
| 'mindmap'
| 'architecture'
- | 'sequence'
| 'image'
| 'class'
| 'entity'
@@ -231,14 +227,6 @@ export function getAddItemDefinitions(t: TFunction): AddItemDefinition[] {
scope: ['toolbar', 'assets'],
renderIcon: makeLucideIcon(Boxes),
},
- {
- id: 'sequence',
- label: 'Sequence',
- section: 'diagrams',
- keywords: ['sequence', 'diagram', 'participant', 'message', 'uml', 'flow'],
- scope: ['toolbar', 'assets'],
- renderIcon: makeLucideIcon(ArrowRightLeft),
- },
{
id: 'browser',
label: 'Browser',
@@ -257,20 +245,12 @@ export function getAddItemDefinitions(t: TFunction): AddItemDefinition[] {
},
{
id: 'sticky-note',
- label: t('toolbar.stickyNote', 'Sticky Note'),
+ label: t('toolbar.stickyNote', 'Note'),
section: 'other',
keywords: ['sticky note', 'note', 'comment', 'annotation'],
scope: ['toolbar', 'assets'],
renderIcon: makeLucideIcon(StickyNote),
},
- {
- id: 'section',
- label: t('toolbar.section', 'Section'),
- section: 'other',
- keywords: ['section', 'group', 'container'],
- scope: ['toolbar', 'assets'],
- renderIcon: makeLucideIcon(Group),
- },
{
id: 'text',
label: t('toolbar.text', 'Text'),
@@ -340,9 +320,6 @@ export function executeAddItem(
case 'text':
actions.onAddTextNode(position);
return;
- case 'section':
- actions.onAddSection(position);
- return;
case 'journey':
actions.onAddJourneyNode(position);
return;
@@ -352,9 +329,6 @@ export function executeAddItem(
case 'architecture':
actions.onAddArchitectureNode(position);
return;
- case 'sequence':
- actions.onAddSequenceParticipant(position);
- return;
case 'image':
actions.onRequestImageUpload?.();
return;
diff --git a/src/components/command-bar/ImportSurfacePrimitives.tsx b/src/components/command-bar/ImportSurfacePrimitives.tsx
index 62ccba94..c1ad811c 100644
--- a/src/components/command-bar/ImportSurfacePrimitives.tsx
+++ b/src/components/command-bar/ImportSurfacePrimitives.tsx
@@ -1,9 +1,8 @@
import React from 'react';
-import { Check, FileText, XCircle, Eye } from 'lucide-react';
+import { Check, FileText, XCircle } from 'lucide-react';
import { Button } from '@/components/ui/Button';
import { formatBytes } from './importDetection';
import type { NativeParseResult } from './importNativeParsers';
-import { DiagramMiniPreview } from './DiagramMiniPreview';
interface ImportFileBadgeProps {
fileInfo: { name: string; size: number };
@@ -64,8 +63,6 @@ export function NativeParseResultCard({
appliedFeedback,
onApply,
}: NativeParseResultCardProps): React.ReactElement {
- const [showPreview, setShowPreview] = React.useState(true);
-
return (
@@ -79,22 +76,8 @@ export function NativeParseResultCard({
{nativeResult.nodeCount} nodes
{nativeResult.edgeCount} edges
- setShowPreview((v) => !v)}
- className="ml-auto flex items-center gap-1 text-[var(--brand-secondary)] hover:text-[var(--brand-text)] transition-colors"
- >
-
- {showPreview ? 'Hide preview' : 'Preview'}
-
- {showPreview && (
-
-
-
- )}
-
{previewDslLabel}
diff --git a/src/components/command-bar/RootView.tsx b/src/components/command-bar/RootView.tsx
index 14ebef1a..0306ffb4 100644
--- a/src/components/command-bar/RootView.tsx
+++ b/src/components/command-bar/RootView.tsx
@@ -69,7 +69,14 @@ const CommandItemRow = ({
-
{item.label}
+
+ {item.label}
+ {item.badge && (
+
+ {item.badge}
+
+ )}
+
{item.description && (
void;
onOpenStudioMermaid?: () => void;
onOpenStudioPlayback?: () => void;
+ onOpenArchitectureRules?: () => void;
initialView?: CommandView;
onAddAnnotation?: () => void;
onAddSection?: () => void;
diff --git a/src/components/command-bar/useCommandBarCommands.test.tsx b/src/components/command-bar/useCommandBarCommands.test.tsx
index 2b464d90..b2814d1e 100644
--- a/src/components/command-bar/useCommandBarCommands.test.tsx
+++ b/src/components/command-bar/useCommandBarCommands.test.tsx
@@ -7,6 +7,7 @@ describe('useCommandBarCommands', () => {
const onOpenStudioAI = vi.fn();
const onOpenStudioOpenFlow = vi.fn();
const onOpenStudioMermaid = vi.fn();
+ const onOpenArchitectureRules = vi.fn();
const { result } = renderHook(() =>
useCommandBarCommands({
@@ -21,6 +22,7 @@ describe('useCommandBarCommands', () => {
onOpenStudioAI,
onOpenStudioOpenFlow,
onOpenStudioMermaid,
+ onOpenArchitectureRules,
hasImport: true,
})
);
@@ -33,6 +35,7 @@ describe('useCommandBarCommands', () => {
'assets',
'search-nodes',
'layout',
+ 'architecture-rules',
'studio-openflow',
'studio-mermaid',
'toggle-grid',
@@ -50,10 +53,12 @@ describe('useCommandBarCommands', () => {
expect(result.current.find((command) => command.id === 'studio-openflow')?.tier).toBe('advanced');
result.current.find((command) => command.id === 'studio-ai')?.action?.();
+ result.current.find((command) => command.id === 'architecture-rules')?.action?.();
result.current.find((command) => command.id === 'studio-openflow')?.action?.();
result.current.find((command) => command.id === 'studio-mermaid')?.action?.();
expect(onOpenStudioAI).toHaveBeenCalledTimes(1);
+ expect(onOpenArchitectureRules).toHaveBeenCalledTimes(1);
expect(onOpenStudioOpenFlow).toHaveBeenCalledTimes(1);
expect(onOpenStudioMermaid).toHaveBeenCalledTimes(1);
});
diff --git a/src/components/command-bar/useCommandBarCommands.tsx b/src/components/command-bar/useCommandBarCommands.tsx
index 8effa11b..5e61b6e3 100644
--- a/src/components/command-bar/useCommandBarCommands.tsx
+++ b/src/components/command-bar/useCommandBarCommands.tsx
@@ -7,6 +7,7 @@ import {
Import,
Search,
Settings,
+ Shield,
WandSparkles,
Workflow,
} from 'lucide-react';
@@ -22,6 +23,7 @@ interface UseCommandBarCommandsParams {
onOpenStudioAI?: CommandBarProps['onOpenStudioAI'];
onOpenStudioOpenFlow?: CommandBarProps['onOpenStudioOpenFlow'];
onOpenStudioMermaid?: CommandBarProps['onOpenStudioMermaid'];
+ onOpenArchitectureRules?: CommandBarProps['onOpenArchitectureRules'];
hasImport?: boolean;
}
@@ -32,9 +34,50 @@ export function useCommandBarCommands({
onOpenStudioAI,
onOpenStudioOpenFlow,
onOpenStudioMermaid,
+ onOpenArchitectureRules,
hasImport = false,
}: UseCommandBarCommandsParams): CommandItem[] {
return useMemo(() => {
+ const importCommands: CommandItem[] = hasImport
+ ? [{
+ id: 'import',
+ label: 'Import from data',
+ icon: ,
+ tier: 'core',
+ type: 'navigation',
+ view: 'import',
+ description: 'SQL -> ERD, Terraform -> Cloud, OpenAPI -> Sequence, Code -> Architecture',
+ badge: 'Beta',
+ }]
+ : [];
+
+ const settingsCommands: CommandItem[] = settings
+ ? [
+ {
+ id: 'toggle-grid',
+ label: 'Show Grid',
+ icon: ,
+ tier: 'advanced',
+ type: 'toggle',
+ value: settings.showGrid,
+ action: settings.onToggleGrid,
+ description: settings.showGrid ? 'On' : 'Off',
+ hidden: true,
+ },
+ {
+ id: 'toggle-snap',
+ label: 'Snap to Grid',
+ icon: ,
+ tier: 'advanced',
+ type: 'toggle',
+ value: settings.snapToGrid,
+ action: settings.onToggleSnap,
+ description: settings.snapToGrid ? 'On' : 'Off',
+ hidden: true,
+ },
+ ]
+ : [];
+
return [
{
id: 'studio-ai',
@@ -44,16 +87,9 @@ export function useCommandBarCommands({
type: 'action',
description: `Open ${FLOWPILOT_NAME} in the right rail`,
action: onOpenStudioAI,
+ badge: 'Beta',
},
- ...(hasImport ? [{
- id: 'import',
- label: 'Import from data',
- icon: ,
- tier: 'core' as const,
- type: 'navigation' as const,
- view: 'import' as const,
- description: 'SQL -> ERD, Terraform -> Cloud, OpenAPI -> Sequence, Code -> Architecture',
- }] : []),
+ ...importCommands,
{
id: 'templates',
label: 'Start from Template',
@@ -91,6 +127,15 @@ export function useCommandBarCommands({
view: 'layout',
description: 'Arrange the current flow automatically',
},
+ {
+ id: 'architecture-rules',
+ label: 'Architecture Rules',
+ icon: ,
+ tier: 'advanced',
+ type: 'action',
+ description: 'Open architecture guardrails and rule templates',
+ action: onOpenArchitectureRules,
+ },
{
id: 'studio-openflow',
label: 'Edit Flow DSL',
@@ -109,32 +154,7 @@ export function useCommandBarCommands({
description: 'Open Mermaid editing in Studio',
action: onOpenStudioMermaid,
},
- ...(settings
- ? [
- {
- id: 'toggle-grid',
- label: 'Show Grid',
- icon: ,
- tier: 'advanced' as const,
- type: 'toggle' as const,
- value: settings.showGrid,
- action: settings.onToggleGrid,
- description: settings.showGrid ? 'On' : 'Off',
- hidden: true,
- },
- {
- id: 'toggle-snap',
- label: 'Snap to Grid',
- icon: ,
- tier: 'advanced' as const,
- type: 'toggle' as const,
- value: settings.snapToGrid,
- action: settings.onToggleSnap,
- description: settings.snapToGrid ? 'On' : 'Off',
- hidden: true,
- },
- ]
- : []),
+ ...settingsCommands,
{
id: 'undo',
label: 'Undo',
@@ -169,5 +189,14 @@ export function useCommandBarCommands({
hidden: true,
},
];
- }, [onOpenStudioAI, onOpenStudioOpenFlow, onOpenStudioMermaid, hasImport, settings, onUndo, onRedo]);
+ }, [
+ hasImport,
+ onOpenArchitectureRules,
+ onOpenStudioAI,
+ onOpenStudioMermaid,
+ onOpenStudioOpenFlow,
+ onRedo,
+ onUndo,
+ settings,
+ ]);
}
diff --git a/src/components/custom-nodes/MindmapNode.tsx b/src/components/custom-nodes/MindmapNode.tsx
index 53059b10..bfb6f7ef 100644
--- a/src/components/custom-nodes/MindmapNode.tsx
+++ b/src/components/custom-nodes/MindmapNode.tsx
@@ -1,5 +1,6 @@
import React, { memo } from 'react';
import { Position } from '@/lib/reactflowCompat';
+import { useShallow } from 'zustand/react/shallow';
import type { LegacyNodeProps } from '@/lib/reactflowCompat';
import type { NodeData } from '@/lib/types';
import { useInlineNodeTextEdit } from '@/hooks/useInlineNodeTextEdit';
@@ -26,13 +27,15 @@ function MindmapNode({ id, data, selected }: LegacyNodeProps): React.R
const visualStyle = resolveNodeVisualStyle(activeColor, activeColorMode, data.customColor);
const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges);
- const { childCount, hiddenDescendantCount } = useFlowStore((state) => {
- const childrenById = getMindmapChildrenById(state.nodes, state.edges);
- return {
- childCount: (childrenById.get(id) ?? []).length,
- hiddenDescendantCount: getMindmapDescendantIds(id, childrenById).size,
- };
- });
+ const { childCount, hiddenDescendantCount } = useFlowStore(
+ useShallow((state) => {
+ const childrenById = getMindmapChildrenById(state.nodes, state.edges);
+ return {
+ childCount: (childrenById.get(id) ?? []).length,
+ hiddenDescendantCount: getMindmapDescendantIds(id, childrenById).size,
+ };
+ })
+ );
const isCollapsed = data.mindmapCollapsed === true;
const branchHandles = [
{ id: 'left', position: Position.Left, side: 'left' as const },
diff --git a/src/components/flow-canvas/FlowCanvasOverlays.tsx b/src/components/flow-canvas/FlowCanvasOverlays.tsx
index 66dccde1..9316cbff 100644
--- a/src/components/flow-canvas/FlowCanvasOverlays.tsx
+++ b/src/components/flow-canvas/FlowCanvasOverlays.tsx
@@ -118,17 +118,10 @@ export function FlowCanvasOverlays({
onSendToBack={contextActions.onSendToBack}
onChangeNodeType={contextActions.onChangeNodeType}
onEditLabel={contextActions.onEditLabel}
- onFitSectionToContents={contextActions.onFitSectionToContents}
- onBringContentsIntoSection={contextActions.onBringContentsIntoSection}
- onReleaseFromSection={contextActions.onReleaseFromSection}
- onToggleSectionLock={contextActions.onToggleSectionLock}
- onToggleSectionHidden={contextActions.onToggleSectionHidden}
canPaste={true}
selectedCount={contextActions.selectedCount}
onAlignNodes={contextActions.onAlignNodes}
onDistributeNodes={contextActions.onDistributeNodes}
- onGroupSelected={contextActions.onGroupSelected}
- onWrapInSection={contextActions.onWrapInSection}
/>
) : null}
diff --git a/src/components/flow-canvas/flowCanvasTypes.test.ts b/src/components/flow-canvas/flowCanvasTypes.test.ts
index 2c970aae..f4591361 100644
--- a/src/components/flow-canvas/flowCanvasTypes.test.ts
+++ b/src/components/flow-canvas/flowCanvasTypes.test.ts
@@ -14,13 +14,11 @@ describe('flowCanvasNodeTypes', () => {
"decision",
"end",
"er_entity",
- "group",
"image",
"journey",
"mindmap",
"mobile",
"process",
- "section",
"sequence_note",
"sequence_participant",
"start",
diff --git a/src/components/flow-canvas/flowCanvasTypes.tsx b/src/components/flow-canvas/flowCanvasTypes.tsx
index 983e8b04..3e0fbad8 100644
--- a/src/components/flow-canvas/flowCanvasTypes.tsx
+++ b/src/components/flow-canvas/flowCanvasTypes.tsx
@@ -8,9 +8,7 @@ import {
CustomStraightEdge,
} from '@/components/CustomEdge';
import SequenceMessageEdge from '@/components/custom-edge/SequenceMessageEdge';
-import GroupNode from '@/components/GroupNode';
import ImageNode from '@/components/ImageNode';
-import SectionNode from '@/components/SectionNode';
import SwimlaneNode from '@/components/SwimlaneNode';
import TextNode from '@/components/TextNode';
import BrowserNode from '@/components/custom-nodes/BrowserNode';
@@ -35,9 +33,7 @@ export const flowCanvasNodeTypes: NodeTypes = {
journey: JourneyNode,
architecture: ArchitectureNode,
annotation: AnnotationNode,
- section: SectionNode,
text: TextNode,
- group: GroupNode,
swimlane: SwimlaneNode,
image: ImageNode,
browser: BrowserNode,
diff --git a/src/components/flow-editor/FlowEditorChrome.tsx b/src/components/flow-editor/FlowEditorChrome.tsx
index d0f621fe..28a89467 100644
--- a/src/components/flow-editor/FlowEditorChrome.tsx
+++ b/src/components/flow-editor/FlowEditorChrome.tsx
@@ -1,6 +1,7 @@
import React, { Suspense, lazy } from 'react';
import type { NodeData } from '@/lib/types';
import type { FlowEditorPanelsProps } from '@/components/FlowEditorPanels';
+import type { CinematicExportRequest } from '@/services/export/cinematicExport';
import type {
CollaborationRemotePresence,
FlowEditorCollaborationTopNavState,
@@ -68,7 +69,7 @@ export interface FlowEditorChromeProps {
onExportSVG: () => void;
onCopySVG: () => void;
onExportPDF: () => void;
- onExportCinematic: (format: 'cinematic-video' | 'cinematic-gif') => void;
+ onExportCinematic: (request: CinematicExportRequest) => void;
onExportJSON: () => void;
onCopyJSON: () => void;
onExportMermaid: () => void;
diff --git a/src/components/flow-editor/buildFlowEditorControllerParams.ts b/src/components/flow-editor/buildFlowEditorControllerParams.ts
index 838dd87e..4d4da21c 100644
--- a/src/components/flow-editor/buildFlowEditorControllerParams.ts
+++ b/src/components/flow-editor/buildFlowEditorControllerParams.ts
@@ -2,6 +2,7 @@ import type { TFunction } from 'i18next';
import type { FlowEdge, FlowNode, FlowSnapshot } from '@/lib/types';
import type { FlowEditorMode, StudioCodeMode, StudioTab } from '@/hooks/useFlowEditorUIState';
import type { DomainLibraryItem } from '@/services/domainLibrary';
+import type { CinematicExportRequest } from '@/services/export/cinematicExport';
import type {
UseFlowEditorChromeParams,
UseFlowEditorPanelsParams,
@@ -25,6 +26,7 @@ interface BuildFlowEditorControllerShellParams {
isCommandBarOpen: boolean;
isHistoryOpen: boolean;
editorMode: FlowEditorMode;
+ isArchitectureRulesOpen: boolean;
handleExportJSON: () => void;
onLayout: (
direction?: 'TB' | 'LR' | 'RL' | 'BT',
@@ -41,6 +43,7 @@ interface BuildFlowEditorControllerStudioParams {
setStudioTab: (tab: StudioTab) => void;
setStudioCodeMode: (mode: StudioCodeMode) => void;
setStudioMode: () => void;
+ openArchitectureRulesPanel: () => void;
closeCommandBar: () => void;
setCanvasMode: () => void;
setSelectedNodeId: (id: string | null) => void;
@@ -60,7 +63,7 @@ interface BuildFlowEditorControllerChromeParams {
handleSvgExport: () => void;
handleCopySvg: () => void;
handlePdfExport: () => void;
- handleCinematicExport: (format: 'cinematic-video' | 'cinematic-gif') => void;
+ handleCinematicExport: (request: CinematicExportRequest) => void;
handleExportJSON: () => void;
handleCopyJSON: () => void;
handleExportMermaid: () => void;
diff --git a/src/components/flow-editor/chromePropTypes.ts b/src/components/flow-editor/chromePropTypes.ts
index 7f57d775..75465e52 100644
--- a/src/components/flow-editor/chromePropTypes.ts
+++ b/src/components/flow-editor/chromePropTypes.ts
@@ -1,5 +1,6 @@
import type { TFunction } from 'i18next';
import type { FlowNode } from '@/lib/types';
+import type { CinematicExportRequest } from '@/services/export/cinematicExport';
import type { FlowEditorChromeProps } from './FlowEditorChrome';
export interface BuildTopNavParams {
@@ -13,7 +14,7 @@ export interface BuildTopNavParams {
handleSvgExport: () => void;
handleCopySvg: () => void;
handlePdfExport: () => void;
- handleCinematicExport: (format: 'cinematic-video' | 'cinematic-gif') => void;
+ handleCinematicExport: (request: CinematicExportRequest) => void;
handleExportJSON: () => void;
handleCopyJSON: () => void;
handleExportMermaid: () => void;
diff --git a/src/components/flow-editor/panelProps.test.ts b/src/components/flow-editor/panelProps.test.ts
index c950556e..eec2a4e8 100644
--- a/src/components/flow-editor/panelProps.test.ts
+++ b/src/components/flow-editor/panelProps.test.ts
@@ -48,6 +48,7 @@ describe('buildFlowEditorPanelsProps', () => {
openStudioAI: vi.fn(),
openStudioCode,
openStudioPlayback: vi.fn(),
+ openArchitectureRulesPanel: vi.fn(),
commandBarView: 'assets' as const,
handleAddAnnotation: vi.fn(),
handleAddSection: vi.fn(),
@@ -131,6 +132,7 @@ describe('buildFlowEditorPanelsProps', () => {
lastAIError: null,
onClearAIError: vi.fn(),
chatMessages: [],
+ assistantThread: [],
clearChat: vi.fn(),
selectedNode: nodes[0],
selectedNodeCount: nodes.length,
@@ -158,6 +160,10 @@ describe('buildFlowEditorPanelsProps', () => {
snapshots: snapshotsPanel,
properties,
studio,
+ architectureRules: {
+ isOpen: false,
+ closeArchitectureRulesPanel: vi.fn(),
+ },
isHistoryOpen: true,
editorMode: 'studio',
});
diff --git a/src/components/flow-editor/panelProps.ts b/src/components/flow-editor/panelProps.ts
index 87c22da7..5cb7a897 100644
--- a/src/components/flow-editor/panelProps.ts
+++ b/src/components/flow-editor/panelProps.ts
@@ -10,6 +10,7 @@ import type { LayoutAlgorithm } from '@/services/elkLayout';
import type { FlowTemplate } from '@/services/templates';
import type { DomainLibraryItem } from '@/services/domainLibrary';
import type { ChatMessage } from '@/services/aiService';
+import type { AssistantThreadItem } from '@/services/flowpilot/types';
import type { CodebaseAnalysis } from '@/hooks/ai-generation/codebaseAnalyzer';
import type { SupportedLanguage } from '@/hooks/ai-generation/codeToArchitecture';
import type { TerraformInputFormat } from '@/hooks/ai-generation/terraformToCloud';
@@ -31,6 +32,7 @@ export interface CommandBarPanelBuilderParams {
openStudioAI: () => void;
openStudioCode: (codeMode: StudioCodeMode) => void;
openStudioPlayback: () => void;
+ openArchitectureRulesPanel: () => void;
commandBarView: CommandBarView;
handleAddAnnotation: () => void;
handleAddSection: () => void;
@@ -115,6 +117,7 @@ export interface StudioRailBuilderParams {
lastAIError: string | null;
onClearAIError: () => void;
chatMessages: ChatMessage[];
+ assistantThread: AssistantThreadItem[];
clearChat: () => void;
selectedNode: FlowNode | null;
selectedNodeCount: number;
@@ -133,6 +136,10 @@ export interface BuildFlowEditorPanelsPropsParams {
snapshots: SnapshotsPanelBuilderParams;
properties: PropertiesRailBuilderParams;
studio: StudioRailBuilderParams;
+ architectureRules: {
+ isOpen: boolean;
+ closeArchitectureRulesPanel: () => void;
+ };
isHistoryOpen: boolean;
editorMode: FlowEditorMode;
}
@@ -161,6 +168,7 @@ export function buildCommandBarPanelProps({
openStudioAI,
openStudioCode,
openStudioPlayback,
+ openArchitectureRulesPanel,
commandBarView,
handleAddAnnotation,
handleAddSection,
@@ -198,6 +206,7 @@ export function buildCommandBarPanelProps({
onOpenStudioOpenFlow: () => openStudioCode('openflow'),
onOpenStudioMermaid: () => openStudioCode('mermaid'),
onOpenStudioPlayback: openStudioPlayback,
+ onOpenArchitectureRules: openArchitectureRulesPanel,
initialView: commandBarView,
onAddAnnotation: handleAddAnnotation,
onAddSection: handleAddSection,
@@ -326,6 +335,7 @@ export function buildStudioRailProps({
lastAIError,
onClearAIError,
chatMessages,
+ assistantThread,
clearChat,
selectedNode,
selectedNodeCount,
@@ -353,6 +363,7 @@ export function buildStudioRailProps({
lastAIError,
onClearAIError,
chatMessages,
+ assistantThread,
onClearChat: clearChat,
selectedNode,
selectedNodeCount,
@@ -372,6 +383,7 @@ export function buildFlowEditorPanelsProps({
snapshots,
properties,
studio,
+ architectureRules,
isHistoryOpen,
editorMode,
}: BuildFlowEditorPanelsPropsParams): FlowEditorPanelsProps {
@@ -380,6 +392,10 @@ export function buildFlowEditorPanelsProps({
snapshots: buildSnapshotsPanelProps(snapshots),
properties: buildPropertiesRailProps(properties),
studio: buildStudioRailProps(studio),
+ architectureRules: {
+ isOpen: architectureRules.isOpen,
+ onClose: architectureRules.closeArchitectureRulesPanel,
+ },
isHistoryOpen,
editorMode,
};
diff --git a/src/components/flow-editor/useFlowEditorController.ts b/src/components/flow-editor/useFlowEditorController.ts
index 48e6627a..4d94113c 100644
--- a/src/components/flow-editor/useFlowEditorController.ts
+++ b/src/components/flow-editor/useFlowEditorController.ts
@@ -6,6 +6,7 @@ import {
} from '@/app/routeState';
import { getFlowTemplates } from '@/services/templates';
import type { FlowEdge, FlowNode, FlowSnapshot } from '@/lib/types';
+import type { CinematicExportRequest } from '@/services/export/cinematicExport';
import type { FlowEditorMode, StudioCodeMode, StudioTab } from '@/hooks/useFlowEditorUIState';
import type { DomainLibraryItem } from '@/services/domainLibrary';
import type { LayoutAlgorithm } from '@/services/elkLayout';
@@ -38,6 +39,7 @@ export interface UseFlowEditorShellParams {
isCommandBarOpen: boolean;
isHistoryOpen: boolean;
editorMode: FlowEditorMode;
+ isArchitectureRulesOpen: boolean;
handleExportJSON: () => void;
onLayout: (
direction?: 'TB' | 'LR' | 'RL' | 'BT',
@@ -54,6 +56,7 @@ export interface UseFlowEditorStudioParams {
setStudioTab: (tab: StudioTab) => void;
setStudioCodeMode: (mode: StudioCodeMode) => void;
setStudioMode: () => void;
+ openArchitectureRulesPanel: () => void;
closeCommandBar: () => void;
setCanvasMode: () => void;
setSelectedNodeId: (id: string | null) => void;
@@ -82,6 +85,7 @@ type FlowEditorCommandBarConfig = Omit<
| 'handleAddImage'
| 'handleAddWireframe'
| 'handleAddDomainLibraryItem'
+ | 'openArchitectureRulesPanel'
>;
type FlowEditorSnapshotsConfig = Omit<
@@ -131,6 +135,10 @@ export interface UseFlowEditorPanelsParams {
snapshots: FlowEditorSnapshotsConfig;
properties: FlowEditorPropertiesConfig;
studio: FlowEditorStudioConfig;
+ architectureRules: {
+ isOpen: boolean;
+ closeArchitectureRulesPanel: () => void;
+ };
isHistoryOpen: BuildFlowEditorPanelsPropsParams['isHistoryOpen'];
editorMode: BuildFlowEditorPanelsPropsParams['editorMode'];
}
@@ -146,7 +154,7 @@ export interface UseFlowEditorChromeParams {
handleSvgExport: () => void;
handleCopySvg: () => void;
handlePdfExport: () => void;
- handleCinematicExport: (format: 'cinematic-video' | 'cinematic-gif') => void;
+ handleCinematicExport: (request: CinematicExportRequest) => void;
handleExportJSON: () => void;
handleCopyJSON: () => void;
handleExportMermaid: () => void;
@@ -222,6 +230,7 @@ export function useFlowEditorController({
openStudioAI,
openStudioCode,
openStudioPlayback,
+ openArchitectureRulesPanel,
toggleStudioPanel,
closeStudioPanel,
handleCanvasEntityIntent,
@@ -243,6 +252,7 @@ export function useFlowEditorController({
openStudioAI,
openStudioCode,
openStudioPlayback,
+ openArchitectureRulesPanel,
handleAddAnnotation: chromeParams.handleAddAnnotation,
handleAddSection: chromeParams.handleAddSection,
handleAddTextNode: chromeParams.handleAddTextNode,
@@ -296,6 +306,10 @@ export function useFlowEditorController({
initialPrompt: panelParams.studio.pendingAIPrompt,
onInitialPromptConsumed: panelParams.studio.clearPendingAIPrompt,
},
+ architectureRules: {
+ isOpen: panelParams.architectureRules.isOpen,
+ closeArchitectureRulesPanel: panelParams.architectureRules.closeArchitectureRulesPanel,
+ },
isHistoryOpen: shell.isHistoryOpen,
editorMode: shell.editorMode,
});
diff --git a/src/components/flow-editor/useFlowEditorScreenModel.ts b/src/components/flow-editor/useFlowEditorScreenModel.ts
index b1b3d653..2e93d7eb 100644
--- a/src/components/flow-editor/useFlowEditorScreenModel.ts
+++ b/src/components/flow-editor/useFlowEditorScreenModel.ts
@@ -46,6 +46,7 @@ export function useFlowEditorScreenModel({ onGoHome }: UseFlowEditorScreenModelP
handleOpenApiAnalysis,
handleCodebaseAnalysis,
chatMessages,
+ assistantThread,
clearChat,
clearLastError,
} = useAIGeneration(screenState.recordHistory, callbacks.handleCommandBarApply);
@@ -157,6 +158,7 @@ export function useFlowEditorScreenModel({ onGoHome }: UseFlowEditorScreenModelP
isCommandBarOpen: screenState.isCommandBarOpen,
isHistoryOpen: screenState.isHistoryOpen,
editorMode: screenState.editorMode,
+ isArchitectureRulesOpen: screenState.isArchitectureRulesOpen,
handleExportJSON,
onLayout,
fileInputRef,
@@ -169,6 +171,7 @@ export function useFlowEditorScreenModel({ onGoHome }: UseFlowEditorScreenModelP
setStudioTab: screenState.setStudioTab,
setStudioCodeMode: screenState.setStudioCodeMode,
setStudioMode: screenState.setStudioMode,
+ openArchitectureRulesPanel: screenState.openArchitectureRulesPanel,
closeCommandBar: screenState.closeCommandBar,
setCanvasMode: screenState.setCanvasMode,
setSelectedNodeId: screenState.setSelectedNodeId,
@@ -240,6 +243,7 @@ export function useFlowEditorScreenModel({ onGoHome }: UseFlowEditorScreenModelP
lastAIError: lastError,
onClearAIError: clearLastError,
chatMessages,
+ assistantThread,
clearChat,
studioCodeMode: screenState.studioCodeMode,
playback: {
@@ -258,6 +262,10 @@ export function useFlowEditorScreenModel({ onGoHome }: UseFlowEditorScreenModelP
pendingAIPrompt,
clearPendingAIPrompt,
},
+ architectureRules: {
+ isOpen: screenState.isArchitectureRulesOpen,
+ closeArchitectureRulesPanel: screenState.closeArchitectureRulesPanel,
+ },
isHistoryOpen: screenState.isHistoryOpen,
editorMode: screenState.editorMode,
},
diff --git a/src/components/flow-editor/useFlowEditorShellController.test.tsx b/src/components/flow-editor/useFlowEditorShellController.test.tsx
index 8a4625a1..c1cc97cf 100644
--- a/src/components/flow-editor/useFlowEditorShellController.test.tsx
+++ b/src/components/flow-editor/useFlowEditorShellController.test.tsx
@@ -48,6 +48,7 @@ describe('useFlowEditorShellController', () => {
isCommandBarOpen: false,
isHistoryOpen: false,
editorMode: 'canvas',
+ isArchitectureRulesOpen: false,
handleExportJSON: vi.fn(),
onLayout: vi.fn(async () => undefined),
}));
@@ -80,6 +81,7 @@ describe('useFlowEditorShellController', () => {
isCommandBarOpen: false,
isHistoryOpen: false,
editorMode: 'canvas',
+ isArchitectureRulesOpen: false,
handleExportJSON: vi.fn(),
onLayout,
}));
diff --git a/src/components/flow-editor/useFlowEditorShellController.ts b/src/components/flow-editor/useFlowEditorShellController.ts
index 26f6be41..cbbb2e3d 100644
--- a/src/components/flow-editor/useFlowEditorShellController.ts
+++ b/src/components/flow-editor/useFlowEditorShellController.ts
@@ -42,6 +42,7 @@ interface UseFlowEditorShellControllerParams {
isCommandBarOpen: boolean;
isHistoryOpen: boolean;
editorMode: FlowEditorMode;
+ isArchitectureRulesOpen: boolean;
handleExportJSON: () => void;
onLayout: (
direction?: 'TB' | 'LR' | 'RL' | 'BT',
@@ -74,6 +75,7 @@ export function useFlowEditorShellController({
isCommandBarOpen,
isHistoryOpen,
editorMode,
+ isArchitectureRulesOpen,
handleExportJSON,
onLayout,
}: UseFlowEditorShellControllerParams): UseFlowEditorShellControllerResult {
@@ -128,6 +130,7 @@ export function useFlowEditorShellController({
isCommandBarOpen ||
isHistoryOpen ||
editorMode === 'studio' ||
+ isArchitectureRulesOpen ||
Boolean(selectedNode || selectedEdge || selectedNodes.length > 1);
return {
diff --git a/src/components/flow-editor/useFlowEditorStudioController.test.tsx b/src/components/flow-editor/useFlowEditorStudioController.test.tsx
index 72af2d3a..0dce57f2 100644
--- a/src/components/flow-editor/useFlowEditorStudioController.test.tsx
+++ b/src/components/flow-editor/useFlowEditorStudioController.test.tsx
@@ -11,6 +11,7 @@ function createBaseProps(overrides: Partial void;
setStudioCodeMode: (mode: StudioCodeMode) => void;
setStudioMode: () => void;
+ openArchitectureRulesPanel: () => void;
closeCommandBar: () => void;
setCanvasMode: () => void;
setSelectedNodeId: (id: string | null) => void;
@@ -26,6 +27,7 @@ interface UseFlowEditorStudioControllerResult {
openStudioAI: () => void;
openStudioCode: (codeMode: StudioCodeMode) => void;
openStudioPlayback: () => void;
+ openArchitectureRulesPanel: () => void;
toggleStudioPanel: () => void;
closeStudioPanel: () => void;
handleCanvasEntityIntent: () => void;
@@ -39,6 +41,7 @@ export function useFlowEditorStudioController({
setStudioTab,
setStudioCodeMode,
setStudioMode,
+ openArchitectureRulesPanel: handleOpenArchitectureRulesPanel,
closeCommandBar,
setCanvasMode,
setSelectedNodeId,
@@ -89,6 +92,12 @@ export function useFlowEditorStudioController({
openStudioPanel('playback', { closeLauncher: true });
}, [openStudioPanel]);
+ const openArchitectureRulesPanel = useCallback(() => {
+ captureStudioSelectionSnapshot();
+ handleOpenArchitectureRulesPanel();
+ closeCommandBar();
+ }, [captureStudioSelectionSnapshot, closeCommandBar, handleOpenArchitectureRulesPanel]);
+
const toggleStudioPanel = useCallback(() => {
if (editorMode === 'studio') {
clearSelectionAndSetCanvasMode();
@@ -125,6 +134,7 @@ export function useFlowEditorStudioController({
openStudioAI,
openStudioCode,
openStudioPlayback,
+ openArchitectureRulesPanel,
toggleStudioPanel,
closeStudioPanel,
handleCanvasEntityIntent,
diff --git a/src/components/home/HomeDashboard.tsx b/src/components/home/HomeDashboard.tsx
index e7de4809..63c551ce 100644
--- a/src/components/home/HomeDashboard.tsx
+++ b/src/components/home/HomeDashboard.tsx
@@ -14,11 +14,7 @@ import { useTranslation } from 'react-i18next';
import { Button } from '../ui/Button';
import { Tooltip } from '../Tooltip';
import type { WorkspaceDocumentPreview } from '@/store/workspaceDocumentModel';
-import {
- getRecentOnboardingActionSuggestions,
- recordOnboardingEvent,
- type OnboardingActionSuggestion,
-} from '@/services/onboarding/events';
+import { recordOnboardingEvent } from '@/services/onboarding/events';
const AUTOSAVED_LABEL = 'Autosaved';
@@ -56,32 +52,15 @@ export function HomeDashboard({
onDeleteFlow,
}: HomeDashboardProps): React.ReactElement {
const { t } = useTranslation();
+ const hasFlows = flows.length > 0;
const secondaryActionIconClass =
'h-4 w-4 text-[var(--brand-secondary)] transition-transform duration-300 group-hover:scale-110';
- const recentActionSuggestions = getRecentOnboardingActionSuggestions();
function handleCreateNew(): void {
recordOnboardingEvent('welcome_blank_selected', { source: 'home-dashboard' });
onCreateNew();
}
- const handleCreateNewShortcut = React.useEffectEvent((): void => {
- handleCreateNew();
- });
-
- React.useEffect(() => {
- function handleKeyDown(e: KeyboardEvent): void {
- if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
- e.preventDefault();
- handleCreateNewShortcut();
- }
- }
-
- window.addEventListener('keydown', handleKeyDown);
-
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, []);
-
function handlePromptWithAI(): void {
recordOnboardingEvent('welcome_prompt_selected', { source: 'home-dashboard' });
onPromptWithAI();
@@ -97,23 +76,6 @@ export function HomeDashboard({
onOpenTemplates();
}
- function handleRecentActionSelection(action: OnboardingActionSuggestion['action']): void {
- switch (action) {
- case 'blank':
- handleCreateNew();
- return;
- case 'ai':
- handlePromptWithAI();
- return;
- case 'import':
- handleImportJSON();
- return;
- case 'templates':
- handleOpenTemplates();
- return;
- }
- }
-
return (
@@ -125,20 +87,6 @@ export function HomeDashboard({
{t('home.description', 'Manage your flows and diagrams.')}
-
- }
- >
- {t('common.createNew', 'Create New')}
-
- ⌘N
-
-
-
@@ -164,14 +112,14 @@ export function HomeDashboard({
- {flows.length > 0 && (
+ {hasFlows && (
{flows.length} {t('home.files', 'files')}
)}
- {flows.length === 0 ? (
+ {!hasFlows ? (
- {recentActionSuggestions.length > 0 ? (
-
-
- {t('home.continueTitle', 'Continue with a recent action')}
-
-
- {recentActionSuggestions.map((suggestion) => {
- const suggestionCopy = getSuggestionCopy(t, suggestion.action);
-
- return (
-
handleRecentActionSelection(suggestion.action)}
- className="rounded-[14px] border border-[var(--color-brand-border)]/60 bg-[var(--brand-surface)] px-4 py-3 text-left transition-colors hover:border-[var(--brand-primary)]/40 hover:bg-[var(--brand-primary)]/5 focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand-primary)]/40"
- >
-
- {suggestionCopy.label}
-
-
- {suggestionCopy.description}
-
-
- );
- })}
-
-
- ) : null}
-
['t'],
- action: OnboardingActionSuggestion['action']
-): SuggestionCopy {
- switch (action) {
- case 'blank':
- return {
- label: t('home.suggestionBlank', 'Blank canvas'),
- description: t('home.suggestionBlankDesc', 'Jump straight into the editor'),
- };
- case 'ai':
- return {
- label: t('home.suggestionAI', 'Flowpilot AI'),
- description: t('home.suggestionAIDesc', 'Start from a prompt'),
- };
- case 'import':
- return {
- label: t('home.suggestionImport', 'Import'),
- description: t('home.suggestionImportDesc', 'Bring in existing work'),
- };
- case 'templates':
- return {
- label: t('home.suggestionTemplates', 'Templates'),
- description: t('home.suggestionTemplatesDesc', 'Start from a proven pattern'),
- };
- }
-}
-
function formatUpdatedAt(updatedAt?: string): string {
if (!updatedAt) {
return AUTOSAVED_LABEL;
diff --git a/src/components/nodeHelpers.ts b/src/components/nodeHelpers.ts
index c670d8a2..aa4e9c4d 100644
--- a/src/components/nodeHelpers.ts
+++ b/src/components/nodeHelpers.ts
@@ -47,10 +47,19 @@ export function getMinNodeSize(shape: NodeData['shape'] | undefined): {
minWidth: number;
minHeight: number;
} {
- if (shape === 'circle' || shape === 'ellipse') return { minWidth: 120, minHeight: 120 };
- if (shape === 'diamond' || shape === 'hexagon') return { minWidth: 140, minHeight: 140 };
- if (shape === 'parallelogram' || shape === 'cylinder') return { minWidth: 140, minHeight: 80 };
- return { minWidth: 120, minHeight: 60 };
+ switch (shape) {
+ case 'circle':
+ case 'ellipse':
+ return { minWidth: 120, minHeight: 120 };
+ case 'diamond':
+ case 'hexagon':
+ return { minWidth: 140, minHeight: 140 };
+ case 'parallelogram':
+ case 'cylinder':
+ return { minWidth: 140, minHeight: 80 };
+ default:
+ return { minWidth: 120, minHeight: 60 };
+ }
}
export function getIconAssetNodeMinSize(hasLabel: boolean): {
diff --git a/src/components/templates/TemplatePresentation.tsx b/src/components/templates/TemplatePresentation.tsx
index 309ee56c..3cc30def 100644
--- a/src/components/templates/TemplatePresentation.tsx
+++ b/src/components/templates/TemplatePresentation.tsx
@@ -201,7 +201,7 @@ function createTemplatePreview(template: FlowTemplate): {
}
function isPreviewContainerNode(node: FlowNode): boolean {
- return node.type === 'group' || node.type === 'section' || node.type === 'swimlane';
+ return node.type === 'swimlane';
}
function clamp(value: number, min: number, max: number): number {
diff --git a/src/components/top-nav/TopNavActions.tsx b/src/components/top-nav/TopNavActions.tsx
index ea33410c..fa8aac32 100644
--- a/src/components/top-nav/TopNavActions.tsx
+++ b/src/components/top-nav/TopNavActions.tsx
@@ -2,6 +2,8 @@ import React, { lazy, Suspense } from 'react';
import { Play, Share2 } from 'lucide-react';
import type { TFunction } from 'i18next';
import { useTranslation } from 'react-i18next';
+import { useTheme } from '@/context/ThemeContext';
+import type { CinematicExportRequest } from '@/services/export/cinematicExport';
import { ExportMenu } from '@/components/ExportMenu';
import { Tooltip } from '@/components/Tooltip';
import { Button } from '@/components/ui/Button';
@@ -33,7 +35,7 @@ interface TopNavActionsProps {
onExportSVG: () => void;
onCopySVG: () => void;
onExportPDF: () => void;
- onExportCinematic: (format: 'cinematic-video' | 'cinematic-gif') => void;
+ onExportCinematic: (request: CinematicExportRequest) => void;
onExportJSON: () => void;
onCopyJSON: () => void;
onExportMermaid: () => void;
@@ -124,6 +126,7 @@ export function TopNavActions({
isBeveled,
}: TopNavActionsProps): React.ReactElement {
const { t } = useTranslation();
+ const { resolvedTheme } = useTheme();
const playLabel = t('common.play', 'Preview');
const [isShareModalOpen, setIsShareModalOpen] = React.useState(false);
const viewerCount = collaboration?.viewerCount ?? 1;
@@ -206,6 +209,7 @@ export function TopNavActions({
onExportFigma={onExportFigma}
onDownloadFigma={onDownloadFigma}
onShare={onShare}
+ cinematicThemeMode={resolvedTheme}
/>
diff --git a/src/components/ui/Switch.tsx b/src/components/ui/Switch.tsx
index d130607c..8a785b30 100644
--- a/src/components/ui/Switch.tsx
+++ b/src/components/ui/Switch.tsx
@@ -5,6 +5,7 @@ interface SwitchProps {
onCheckedChange: (checked: boolean) => void;
label?: string;
className?: string;
+ disabled?: boolean;
}
export const Switch: React.FC
= ({
@@ -12,27 +13,32 @@ export const Switch: React.FC = ({
onCheckedChange,
label,
className = '',
+ disabled = false,
}) => {
return (
-
+
onCheckedChange(e.target.checked)}
/>
{label && (
-
+
{label}
)}
diff --git a/src/components/useExportMenu.test.tsx b/src/components/useExportMenu.test.tsx
index 8589375f..d3739b6c 100644
--- a/src/components/useExportMenu.test.tsx
+++ b/src/components/useExportMenu.test.tsx
@@ -1,9 +1,16 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { CinematicExportRequest } from '@/services/export/cinematicExport';
import { useExportMenu } from './useExportMenu';
const addToast = vi.fn();
+const cinematicRequest: CinematicExportRequest = {
+ format: 'cinematic-video',
+ speed: 'normal',
+ resolution: '1080p',
+ themeMode: 'light',
+};
vi.mock('./ui/ToastContext', () => ({
useToast: () => ({
@@ -18,6 +25,7 @@ const baseProps = {
onCopySVG: vi.fn(),
onExportPDF: vi.fn(),
onExportCinematic: vi.fn(),
+ getCinematicExportRequest: vi.fn(() => cinematicRequest),
onExportJSON: vi.fn(),
onCopyJSON: vi.fn(),
onExportMermaid: vi.fn(),
diff --git a/src/components/useExportMenu.ts b/src/components/useExportMenu.ts
index b1b35471..3fa0d5ca 100644
--- a/src/components/useExportMenu.ts
+++ b/src/components/useExportMenu.ts
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import type { RefObject } from 'react';
+import type { CinematicExportRequest } from '@/services/export/cinematicExport';
import { captureAnalyticsEvent } from '@/services/analytics/analytics';
import { recordOnboardingEvent } from '@/services/onboarding/events';
import { useToast } from './ui/ToastContext';
@@ -10,7 +11,8 @@ interface UseExportMenuParams {
onExportSVG: () => void;
onCopySVG: () => void;
onExportPDF: () => void;
- onExportCinematic: (format: 'cinematic-video' | 'cinematic-gif') => void;
+ onExportCinematic: (request: CinematicExportRequest) => void;
+ getCinematicExportRequest: () => CinematicExportRequest;
onExportJSON: () => void;
onCopyJSON: () => void;
onExportMermaid: () => void;
@@ -51,6 +53,7 @@ export function useExportMenu({
onCopySVG,
onExportPDF,
onExportCinematic,
+ getCinematicExportRequest,
onExportJSON,
onCopyJSON,
onExportMermaid,
@@ -117,12 +120,8 @@ export function useExportMenu({
svg: { download: onExportSVG, copy: onCopySVG },
pdf: { download: onExportPDF, copy: onExportPDF },
'cinematic-video': {
- download: () => onExportCinematic('cinematic-video'),
- copy: () => onExportCinematic('cinematic-video'),
- },
- 'cinematic-gif': {
- download: () => onExportCinematic('cinematic-gif'),
- copy: () => onExportCinematic('cinematic-gif'),
+ download: () => onExportCinematic(getCinematicExportRequest()),
+ copy: () => onExportCinematic(getCinematicExportRequest()),
},
json: { download: onExportJSON, copy: onCopyJSON },
openflow: { download: onDownloadOpenFlowDSL, copy: onExportOpenFlowDSL },
@@ -150,10 +149,11 @@ export function useExportMenu({
return;
}
+ closeMenu();
+
try {
await Promise.resolve(actionHandler());
recordSelection(key, action);
- closeMenu();
} catch (error) {
const message = error instanceof Error ? error.message : 'Please try again.';
addToast(`Failed to complete ${key} ${action}: ${message}`, 'error', 5000);
diff --git a/src/context/CinematicExportContext.tsx b/src/context/CinematicExportContext.tsx
index caddb3d2..3da97d8c 100644
--- a/src/context/CinematicExportContext.tsx
+++ b/src/context/CinematicExportContext.tsx
@@ -1,5 +1,6 @@
import React, { createContext, useContext, useMemo, useState } from 'react';
import type { CinematicRenderState } from '@/services/export/cinematicRenderState';
+import type { CinematicExportRequest } from '@/services/export/cinematicExport';
const EMPTY_SET = new Set();
@@ -18,30 +19,84 @@ function createInactiveRenderState(): CinematicRenderState {
};
}
+export type CinematicExportJobStatus =
+ | 'idle'
+ | 'preparing'
+ | 'capturing'
+ | 'encoding'
+ | 'finalizing'
+ | 'done'
+ | 'error'
+ | 'cancelled';
+
+export interface CinematicExportJobState {
+ status: CinematicExportJobStatus;
+ progressPercent: number;
+ completedFrames: number;
+ totalFrames: number;
+ stageLabel: string;
+ canCancel: boolean;
+ request: CinematicExportRequest | null;
+}
+
+function createInactiveJobState(): CinematicExportJobState {
+ return {
+ status: 'idle',
+ progressPercent: 0,
+ completedFrames: 0,
+ totalFrames: 0,
+ stageLabel: '',
+ canCancel: false,
+ request: null,
+ };
+}
+
interface CinematicExportContextValue {
renderState: CinematicRenderState;
+ jobState: CinematicExportJobState;
setRenderState: (nextState: CinematicRenderState) => void;
resetRenderState: () => void;
+ setJobState: React.Dispatch>;
+ resetJobState: () => void;
+ registerCancelHandler: (handler: (() => void) | null) => void;
+ cancelExport: () => void;
}
const INACTIVE_CONTEXT_VALUE: CinematicExportContextValue = {
renderState: createInactiveRenderState(),
+ jobState: createInactiveJobState(),
setRenderState: () => undefined,
resetRenderState: () => undefined,
+ setJobState: () => undefined,
+ resetJobState: () => undefined,
+ registerCancelHandler: () => undefined,
+ cancelExport: () => undefined,
};
const CinematicExportContext = createContext(INACTIVE_CONTEXT_VALUE);
export function CinematicExportProvider({ children }: { children: React.ReactNode }): React.ReactElement {
const [renderState, setRenderStateState] = useState(() => createInactiveRenderState());
+ const [jobState, setJobState] = useState(() => createInactiveJobState());
+ const [cancelHandler, setCancelHandler] = useState<(() => void) | null>(null);
const value = useMemo(() => ({
renderState,
+ jobState,
setRenderState: (nextState) => setRenderStateState(nextState),
resetRenderState: () => {
setRenderStateState(createInactiveRenderState());
},
- }), [renderState]);
+ setJobState,
+ resetJobState: () => {
+ setJobState(createInactiveJobState());
+ setCancelHandler(null);
+ },
+ registerCancelHandler: (handler) => setCancelHandler(() => handler),
+ cancelExport: () => {
+ cancelHandler?.();
+ },
+ }), [cancelHandler, jobState, renderState]);
return (
@@ -58,10 +113,33 @@ export function useCinematicExportState(): CinematicRenderState {
return useCinematicExportContext().renderState;
}
-export function useCinematicExportActions(): Pick {
- const { setRenderState, resetRenderState } = useCinematicExportContext();
+export function useCinematicExportJobState(): CinematicExportJobState {
+ return useCinematicExportContext().jobState;
+}
+
+export function useCinematicExportActions(): Pick<
+ CinematicExportContextValue,
+ | 'setRenderState'
+ | 'resetRenderState'
+ | 'setJobState'
+ | 'resetJobState'
+ | 'registerCancelHandler'
+ | 'cancelExport'
+> {
+ const {
+ setRenderState,
+ resetRenderState,
+ setJobState,
+ resetJobState,
+ registerCancelHandler,
+ cancelExport,
+ } = useCinematicExportContext();
return {
setRenderState,
resetRenderState,
+ setJobState,
+ resetJobState,
+ registerCancelHandler,
+ cancelExport,
};
}
diff --git a/src/diagram-types/stateDiagram/plugin.ts b/src/diagram-types/stateDiagram/plugin.ts
index 940df738..e9cdb06c 100644
--- a/src/diagram-types/stateDiagram/plugin.ts
+++ b/src/diagram-types/stateDiagram/plugin.ts
@@ -1,5 +1,7 @@
import { parseMermaid } from '@/lib/mermaidParser';
import type { DiagramPlugin } from '@/diagram-types/core';
+import type { FlowNode } from '@/lib/types';
+import { setNodeParent } from '@/lib/nodeParent';
function normalizeStateTransitionLabels(input: string): string {
const lines = input.replace(/\r\n/g, '\n').split('\n');
@@ -83,6 +85,8 @@ function parseStateDiagram(input: string) {
const { diagnostics, direction } = collectStateDiagramDiagnostics(input);
const normalizedInput = normalizeStateTransitionLabels(input);
const parsed = parseMermaid(normalizedInput);
+ const withCompositeParents = applyCompositeStateParenting(parsed.nodes as FlowNode[], input);
+ parsed.nodes = withCompositeParents;
if (direction) {
parsed.direction = direction;
}
@@ -95,6 +99,66 @@ function parseStateDiagram(input: string) {
};
}
+function applyCompositeStateParenting(nodes: FlowNode[], input: string): FlowNode[] {
+ const nextNodes = [...nodes];
+ const nodeIndexById = new Map(nextNodes.map((node, index) => [node.id, index]));
+ const compositeStack: string[] = [];
+
+ const lines = input.replace(/\r\n/g, '\n').split('\n');
+ for (const rawLine of lines) {
+ const line = rawLine.trim();
+ if (!line || line.startsWith('%%')) {
+ continue;
+ }
+
+ const compositeMatch = line.match(/^state\s+("?)([^"{]+)\1\s*\{$/i);
+ if (compositeMatch) {
+ const parentId = compositeMatch[2].trim();
+ compositeStack.push(parentId);
+
+ if (!nodeIndexById.has(parentId)) {
+ nodeIndexById.set(parentId, nextNodes.length);
+ nextNodes.push({
+ id: parentId,
+ type: 'state',
+ position: { x: 0, y: 0 },
+ data: { label: parentId },
+ } as FlowNode);
+ }
+ continue;
+ }
+
+ if (/^}\s*$/.test(line) || /^end\s*$/i.test(line)) {
+ compositeStack.pop();
+ continue;
+ }
+
+ const activeParentId = compositeStack[compositeStack.length - 1];
+ if (!activeParentId) {
+ continue;
+ }
+
+ const transitionMatch = line.match(/^(.+?)\s+(<-->|<--|-->|==>|-.->)\s+(.+?)(?:\s*:\s*(.+))?$/);
+ if (!transitionMatch) {
+ continue;
+ }
+
+ const stateIds = [transitionMatch[1].trim(), transitionMatch[3].trim()].filter(
+ (value) => value !== '[*]'
+ );
+
+ for (const stateId of stateIds) {
+ const nodeIndex = nodeIndexById.get(stateId);
+ if (typeof nodeIndex !== 'number') {
+ continue;
+ }
+ nextNodes[nodeIndex] = setNodeParent(nextNodes[nodeIndex], activeParentId);
+ }
+ }
+
+ return nextNodes;
+}
+
export const STATE_DIAGRAM_PLUGIN: DiagramPlugin = {
id: 'stateDiagram',
displayName: 'State Diagram',
diff --git a/src/hooks/ai-generation/chatHistoryStorage.test.ts b/src/hooks/ai-generation/chatHistoryStorage.test.ts
index ece30f4e..c6d4ff31 100644
--- a/src/hooks/ai-generation/chatHistoryStorage.test.ts
+++ b/src/hooks/ai-generation/chatHistoryStorage.test.ts
@@ -83,6 +83,28 @@ describe('chatHistoryStorage', () => {
);
});
+ it('loads assistant thread items with metadata from the repository', async () => {
+ loadChatThread.mockResolvedValue([
+ {
+ id: 'doc-1:1:assistant_plan',
+ documentId: 'doc-1',
+ role: 'model',
+ parts: [{ text: 'Plan first' }],
+ createdAt: '2026-01-01T00:00:01.000Z',
+ threadType: 'assistant_plan',
+ responseMode: 'plan',
+ thinkingState: 'planning',
+ },
+ ]);
+
+ const { loadAssistantThreadHistory } = await import('./chatHistoryStorage');
+ const [item] = await loadAssistantThreadHistory('doc-1');
+
+ expect(item?.type).toBe('assistant_plan');
+ expect(item?.responseMode).toBe('plan');
+ expect(item?.thinkingState).toBe('planning');
+ });
+
it('clears the repository thread and falls back to localStorage cleanup on failure', async () => {
clearChatThread.mockRejectedValueOnce(new Error('boom'));
localStorage.setItem('ofk_chat_history_doc-4', JSON.stringify([
diff --git a/src/hooks/ai-generation/chatHistoryStorage.ts b/src/hooks/ai-generation/chatHistoryStorage.ts
index 7895a871..aa1ed523 100644
--- a/src/hooks/ai-generation/chatHistoryStorage.ts
+++ b/src/hooks/ai-generation/chatHistoryStorage.ts
@@ -1,6 +1,8 @@
import type { ChatMessage } from '@/services/aiService';
import { localFirstRepository, type PersistedChatMessage } from '@/services/storage/localFirstRepository';
import { parseLegacyChatMessagesJson } from '@/services/storage/storageSchemas';
+import type { AssistantThreadItem } from '@/services/flowpilot/types';
+import { assistantThreadToChatMessages } from '@/services/flowpilot/thread';
const STORAGE_KEY_PREFIX = 'ofk_chat_history_';
@@ -15,6 +17,25 @@ function toChatMessages(messages: PersistedChatMessage[]): ChatMessage[] {
}));
}
+function toAssistantThreadItems(messages: PersistedChatMessage[]): AssistantThreadItem[] {
+ return messages.map((message) => ({
+ id: message.id,
+ role: message.role,
+ type: (message.threadType as AssistantThreadItem['type']) ?? (message.role === 'user' ? 'user_message' : 'assistant_applied_result'),
+ content: message.parts.map((part) => part.text ?? '').join(''),
+ createdAt: message.createdAt,
+ responseMode: message.responseMode,
+ thinkingState: message.thinkingState,
+ summary: message.summary,
+ previewTitle: message.previewTitle,
+ previewDetail: message.previewDetail,
+ previewStats: message.previewStats,
+ applied: message.applied,
+ plan: message.plan,
+ assetMatches: message.assetMatches,
+ }));
+}
+
function toPersistedChatMessages(diagramId: string, messages: ChatMessage[]): PersistedChatMessage[] {
const startedAt = Date.now();
@@ -27,6 +48,29 @@ function toPersistedChatMessages(diagramId: string, messages: ChatMessage[]): Pe
}));
}
+function toPersistedThreadItems(
+ diagramId: string,
+ items: AssistantThreadItem[]
+): PersistedChatMessage[] {
+ return items.map((item, index) => ({
+ id: item.id || `${diagramId}:${index}:${item.type}`,
+ documentId: diagramId,
+ role: item.role,
+ parts: [{ text: item.content }],
+ createdAt: item.createdAt,
+ threadType: item.type,
+ responseMode: item.responseMode,
+ thinkingState: item.thinkingState,
+ summary: item.summary,
+ previewTitle: item.previewTitle,
+ previewDetail: item.previewDetail,
+ previewStats: item.previewStats,
+ applied: item.applied,
+ plan: item.plan,
+ assetMatches: item.assetMatches,
+ }));
+}
+
function loadLegacyChatHistory(diagramId: string): ChatMessage[] {
try {
const raw = localStorage.getItem(storageKey(diagramId));
@@ -69,6 +113,32 @@ export async function saveChatHistory(diagramId: string, messages: ChatMessage[]
}
}
+export async function loadAssistantThreadHistory(diagramId: string): Promise {
+ try {
+ const messages = await localFirstRepository.loadChatThread(diagramId);
+ return toAssistantThreadItems(messages);
+ } catch {
+ return parseLegacyChatMessagesJson(localStorage.getItem(storageKey(diagramId))).map((message, index) => ({
+ id: `${diagramId}:legacy:${index}`,
+ role: message.role,
+ type: message.role === 'user' ? 'user_message' : 'assistant_applied_result',
+ content: message.parts.map((part) => part.text ?? '').join(''),
+ createdAt: new Date(Date.now() + index).toISOString(),
+ }));
+ }
+}
+
+export async function saveAssistantThreadHistory(
+ diagramId: string,
+ items: AssistantThreadItem[]
+): Promise {
+ try {
+ await localFirstRepository.replaceChatThread(diagramId, toPersistedThreadItems(diagramId, items));
+ } catch {
+ saveLegacyChatHistory(diagramId, assistantThreadToChatMessages(items));
+ }
+}
+
export async function clearChatHistory(diagramId: string): Promise {
try {
await localFirstRepository.clearChatThread(diagramId);
@@ -76,3 +146,7 @@ export async function clearChatHistory(diagramId: string): Promise {
clearLegacyChatHistory(diagramId);
}
}
+
+export async function clearAssistantThreadHistory(diagramId: string): Promise {
+ await clearChatHistory(diagramId);
+}
diff --git a/src/hooks/ai-generation/codeToArchitecture.ts b/src/hooks/ai-generation/codeToArchitecture.ts
index a73e68fc..a000c774 100644
--- a/src/hooks/ai-generation/codeToArchitecture.ts
+++ b/src/hooks/ai-generation/codeToArchitecture.ts
@@ -101,6 +101,42 @@ function formatInfraFileLines(infraFiles: string[]): string {
return infraFiles.length > 0 ? infraFiles.map((file) => `- ${file}`).join('\n') : '- none detected';
}
+const ICON_HINTS = `AVAILABLE ICON PACKS (use these exact values when the service matches):
+AWS (archProvider: "aws", archIconPackId: "aws-official-starter-v1"):
+ Lambda -> archIconShapeId: "compute-lambda", color: "violet"
+ EC2 -> archIconShapeId: "compute-ec2", color: "violet"
+ ECS/Fargate -> archIconShapeId: "containers-elastic-container-service", color: "violet"
+ API Gateway -> archIconShapeId: "app-integration-api-gateway", color: "violet"
+ S3 -> archIconShapeId: "storage-simple-storage-service", color: "emerald"
+ RDS/Postgres -> archIconShapeId: "database-rds", color: "emerald"
+ DynamoDB -> archIconShapeId: "database-dynamodb", color: "emerald"
+ ElastiCache -> archIconShapeId: "database-elasticache", color: "yellow"
+ SQS -> archIconShapeId: "app-integration-simple-queue-service", color: "amber"
+ SNS -> archIconShapeId: "app-integration-simple-notification-service", color: "amber"
+ CloudFront -> archIconShapeId: "networking-cloudfront", color: "pink"
+ Route 53 -> archIconShapeId: "networking-route-53", color: "pink"
+ Cognito -> archIconShapeId: "security-cognito", color: "slate"
+ CloudWatch -> archIconShapeId: "management-governance-cloudwatch", color: "slate"
+ EKS -> archIconShapeId: "containers-elastic-kubernetes-service", color: "violet"
+
+Azure (archProvider: "azure", archIconPackId: "azure-official-icons-v20"):
+ Azure Functions -> archIconShapeId: "compute-function-apps", color: "violet"
+ AKS -> archIconShapeId: "compute-kubernetes-services", color: "violet"
+ Azure SQL -> archIconShapeId: "databases-azure-sql", color: "emerald"
+ Blob Storage -> archIconShapeId: "storage-storage-accounts", color: "emerald"
+ Service Bus -> archIconShapeId: "integration-service-bus", color: "amber"
+ API Management -> archIconShapeId: "integration-api-management-services", color: "violet"
+
+GCP (archProvider: "gcp", archIconPackId: "gcp-official-icons-v1"):
+ Cloud Run -> archIconShapeId: "compute-cloud-run", color: "violet"
+ GKE -> archIconShapeId: "compute-kubernetes-engine", color: "violet"
+ Cloud SQL -> archIconShapeId: "databases-cloud-sql", color: "emerald"
+ Pub/Sub -> archIconShapeId: "data-analytics-pub-sub", color: "amber"
+ Cloud Storage -> archIconShapeId: "storage-cloud-storage", color: "emerald"
+
+CNCF (archProvider: "cncf", archIconPackId: "cncf-artwork-icons-v1"):
+ Kubernetes -> archIconShapeId: "projects-clusternet", color: "violet"`;
+
export function buildCodeToArchitecturePrompt({
code,
language,
@@ -114,7 +150,7 @@ Guidelines:
- Use [architecture] nodes for databases, caches, queues, external APIs, cloud services, and infrastructure
- Use [system] nodes for application services, classes, modules, controllers, and business logic
- Use [browser] or [mobile] nodes for frontend surfaces if present
-- Use [section] to group related components (e.g. "Frontend", "Backend", "Database Layer")
+- Do not use section or group containers; keep related components adjacent and use labels or subtitles to imply layers such as Frontend, Backend, or Database Layer
- Use [process] nodes for key operations or workflows
- Color by layer:
- blue for frontend and user-facing surfaces
@@ -125,6 +161,9 @@ Guidelines:
- pink for external or third-party services
- Show data flow and dependencies as edges with clear labels like "HTTP/REST", "SQL", "events", or "cache"
- Prefer [architecture] when the code clearly references cloud services or runtime infrastructure
+- When code references a cloud service, use [architecture] with the correct archProvider, archIconPackId, and archIconShapeId from the icon hints below
+
+${ICON_HINTS}
SOURCE CODE (${LANGUAGE_LABELS[language]}):
\`\`\`${language}
@@ -148,7 +187,7 @@ The codebase was analyzed statically — file imports and dependencies were pars
Guidelines:
- Detect the tech stack and platform from imports, dependencies, and infra files
-- Use [section] containers for top-level layers or modules such as Frontend, API, Services, Data, Platform, or External Services
+- Do not use section or group containers for top-level layers or modules such as Frontend, API, Services, Data, Platform, or External Services
- Use [architecture] nodes for databases, caches, queues, external APIs, cloud services, infrastructure, and platform resources
- Use [system] nodes for key files or modules that serve as services, controllers, models, utilities, or internal APIs
- Use [browser] or [mobile] nodes for frontend entry points if present
@@ -158,7 +197,7 @@ Guidelines:
- azure -> archProvider: "azure"
- gcp -> archProvider: "gcp"
- cncf -> archProvider: "cncf"
- - docker -> use [section] or [architecture] service nodes for containers and runtimes
+ - docker -> use [architecture] service nodes for containers and runtimes
- Color by layer:
- blue -> frontend / user-facing
- violet -> backend services / APIs
diff --git a/src/hooks/ai-generation/codebaseToNativeDiagram.test.ts b/src/hooks/ai-generation/codebaseToNativeDiagram.test.ts
index 468d94cd..b616cde7 100644
--- a/src/hooks/ai-generation/codebaseToNativeDiagram.test.ts
+++ b/src/hooks/ai-generation/codebaseToNativeDiagram.test.ts
@@ -41,21 +41,17 @@ function createAnalysis(): CodebaseAnalysis {
}
describe('buildCodebaseNativeDiagram', () => {
- it('builds a native repo structure diagram with grouped sections and cloud services', () => {
+ it('builds a flat native repo structure diagram with cloud services', () => {
const result = buildCodebaseNativeDiagram(createAnalysis());
expect(result.dsl).toContain('flow: "Repository Module Structure"');
- expect(result.dsl).toContain('group "src/api" {');
expect(result.dsl).toContain('[system] file_src_api_routes_ts: routes');
- expect(result.dsl).toContain('group "Platform Services" {');
+ expect(result.dsl).toContain('subLabel: "src/api');
expect(result.dsl).toContain(
'[architecture] svc_s3: S3 { archProvider: "aws", archResourceType: "service", color: "emerald", archIconPackId: "aws-official-starter-v1", archIconShapeId: "storage-simple-storage-service" }'
);
- expect(result.dsl).toContain(
- 'section_src_api ->|request handling (1 import)| section_src_services'
- );
expect(result.nodeCount).toBeGreaterThan(0);
- expect(result.edgeCount).toBeGreaterThan(0);
+ expect(result.edgeCount).toBe(0);
expect(result.sectionCount).toBe(4);
expect(result.platformServiceCount).toBe(1);
});
@@ -76,10 +72,8 @@ describe('buildCodebaseNativeDiagram', () => {
detectedServices: [],
});
- expect(result.dsl).toContain('group "apps/web" {');
- expect(result.dsl).toContain('group "apps/api" {');
- expect(result.dsl).toContain('group "packages/ui" {');
- expect(result.dsl).toContain('section_apps_web ->|HTTP/UI flow (1 import)| section_apps_api');
- expect(result.dsl).toContain('section_apps_web ->|shared code (1 import)| section_packages_ui');
+ expect(result.dsl).toContain('subLabel: "apps/web');
+ expect(result.dsl).toContain('subLabel: "apps/api');
+ expect(result.dsl).toContain('subLabel: "packages/ui');
});
});
diff --git a/src/hooks/ai-generation/codebaseToNativeDiagram.ts b/src/hooks/ai-generation/codebaseToNativeDiagram.ts
index e7fda52e..38c5e5fb 100644
--- a/src/hooks/ai-generation/codebaseToNativeDiagram.ts
+++ b/src/hooks/ai-generation/codebaseToNativeDiagram.ts
@@ -58,13 +58,6 @@ interface SectionModel {
importantFiles: string[];
}
-interface SectionEdge {
- from: string;
- to: string;
- count: number;
- label: string;
-}
-
function slugify(value: string): string {
return value
.trim()
@@ -201,96 +194,6 @@ function buildSections(analysis: CodebaseAnalysis): SectionModel[] {
.sort((left, right) => left.label.localeCompare(right.label));
}
-function buildFileToSectionMap(sections: SectionModel[]): Map {
- const fileToSection = new Map();
- for (const section of sections) {
- for (const file of section.files) {
- fileToSection.set(file, section.id);
- }
- }
- return fileToSection;
-}
-
-function buildSectionEdges(
- analysis: CodebaseAnalysis,
- fileToSection: Map,
- sections: SectionModel[]
-): SectionEdge[] {
- const sectionsById = new Map(sections.map((section) => [section.id, section]));
- const counts = new Map();
-
- for (const edge of analysis.edges) {
- const from = fileToSection.get(edge.from);
- const to = fileToSection.get(edge.to);
- if (!from || !to || from === to) continue;
- const key = `${from}->${to}`;
- counts.set(key, (counts.get(key) ?? 0) + 1);
- }
-
- return [...counts.entries()]
- .map(([key, count]) => {
- const [from, to] = key.split('->');
- const fromSection = sectionsById.get(from);
- const toSection = sectionsById.get(to);
- return {
- from,
- to,
- count,
- label: getSectionEdgeLabel(fromSection, toSection, count),
- };
- })
- .sort((left, right) => right.count - left.count);
-}
-
-function getSectionEdgeLabel(
- fromSection: SectionModel | undefined,
- toSection: SectionModel | undefined,
- count: number
-): string {
- if (!fromSection || !toSection) {
- return formatImportLabel(count);
- }
-
- if (toSection.role === 'config') {
- return formatSemanticLabel('config usage', count);
- }
- if (toSection.role === 'auth') {
- return formatSemanticLabel('auth flow', count);
- }
- if (toSection.role === 'models') {
- return formatSemanticLabel('data access', count);
- }
- if (toSection.role === 'utils') {
- return formatSemanticLabel('shared code', count);
- }
- if (isSharedWorkspaceSection(toSection.label)) {
- return formatSemanticLabel('shared code', count);
- }
- if (fromSection.role === 'frontend' && (toSection.role === 'routes' || toSection.role === 'services')) {
- return formatSemanticLabel('HTTP/UI flow', count);
- }
- if (fromSection.role === 'routes' && toSection.role === 'services') {
- return formatSemanticLabel('request handling', count);
- }
- if (fromSection.role === 'services' && toSection.role === 'services') {
- return formatSemanticLabel('service call', count);
- }
-
- return formatImportLabel(count);
-}
-
-function formatSemanticLabel(label: string, count: number): string {
- return `${label} (${count} import${count === 1 ? '' : 's'})`;
-}
-
-function formatImportLabel(count: number): string {
- return `${count} import${count === 1 ? '' : 's'}`;
-}
-
-function isSharedWorkspaceSection(label: string): boolean {
- return /^(packages|libs)\//.test(label);
-}
-
function buildCloudServiceSection(services: DetectedService[]): string[] {
if (services.length === 0) {
return [];
@@ -301,26 +204,22 @@ function buildCloudServiceSection(services: DetectedService[]): string[] {
return [];
}
- const lines = ['group "Platform Services" {'];
- lines.push(' [section] platform_services: Platform Services { color: "pink" }');
+ const lines: string[] = [];
for (const service of supportedServices.slice(0, 6)) {
const nodeId = `svc_${slugify(service.name)}`;
const iconPackId = service.iconPackId ? `, archIconPackId: "${service.iconPackId}"` : '';
const iconShapeId = service.iconShapeId ? `, archIconShapeId: "${service.iconShapeId}"` : '';
lines.push(
- ` [architecture] ${nodeId}: ${service.name} { archProvider: "${service.provider}", archResourceType: "${service.resourceType}", color: "${service.suggestedColor}"${iconPackId}${iconShapeId} }`
+ `[architecture] ${nodeId}: ${service.name} { archProvider: "${service.provider}", archResourceType: "${service.resourceType}", color: "${service.suggestedColor}"${iconPackId}${iconShapeId} }`
);
}
- lines.push('}');
return lines;
}
function buildNativeDiagramDsl(analysis: CodebaseAnalysis): string {
const sections = buildSections(analysis);
- const fileToSection = buildFileToSectionMap(sections);
- const sectionEdges = buildSectionEdges(analysis, fileToSection, sections);
const lines = [
`flow: ${quoteLabel('Repository Module Structure')}`,
'direction: TB',
@@ -328,21 +227,17 @@ function buildNativeDiagramDsl(analysis: CodebaseAnalysis): string {
];
for (const section of sections) {
- lines.push(`group ${quoteLabel(section.label)} {`);
- lines.push(` [section] ${section.id}: ${section.label} { color: "${section.color}" }`);
-
for (const path of section.importantFiles) {
const fileRole = detectFileRole(path);
const nodeId = `file_${slugify(path)}`;
const label = formatFileLabel(path);
const visual = ROLE_VISUAL[section.role];
- const subLabel = path.replace(/"/g, '\\"');
+ const subLabel = `${section.label} · ${path}`.replace(/"/g, '\\"');
lines.push(
- ` [${visual.nodeType}] ${nodeId}: ${label} { icon: "${FILE_ROLE_ICON[fileRole]}", color: "${section.color}", subLabel: "${subLabel}" }`
+ `[${visual.nodeType}] ${nodeId}: ${label} { icon: "${FILE_ROLE_ICON[fileRole]}", color: "${section.color}", subLabel: "${subLabel}" }`
);
}
- lines.push('}');
lines.push('');
}
@@ -351,10 +246,6 @@ function buildNativeDiagramDsl(analysis: CodebaseAnalysis): string {
lines.push(...cloudLines, '');
}
- for (const edge of sectionEdges.slice(0, 18)) {
- lines.push(`${edge.from} ->|${edge.label}| ${edge.to}`);
- }
-
return lines.join('\n').trim();
}
@@ -365,7 +256,7 @@ function countDiagramElements(dsl: string): { nodeCount: number; edgeCount: numb
for (const line of lines) {
const trimmed = line.trim();
- if (/^\[(entity|process|system|section|start|end|decision|browser|mobile|note|annotation|container|architecture)\]/.test(trimmed)) {
+ if (/^\[(entity|process|system|start|end|decision|browser|mobile|note|annotation|container|architecture)\]/.test(trimmed)) {
nodeCount += 1;
continue;
}
diff --git a/src/hooks/ai-generation/openApiToSequence.ts b/src/hooks/ai-generation/openApiToSequence.ts
index 590e29bf..5e56c5c8 100644
--- a/src/hooks/ai-generation/openApiToSequence.ts
+++ b/src/hooks/ai-generation/openApiToSequence.ts
@@ -20,7 +20,7 @@ Rules:
- Create one [system] node per resource group listed above
- Show 1–2 representative flows per resource group as directed edges (e.g. "GET /users", "POST /orders")
- Do NOT map every endpoint — show only the flows that best explain each group's purpose${authRule}
-- Group related resource nodes into [section] containers by domain
+- Do not use section or group containers; keep related resource nodes adjacent by domain
- Skip: deeply nested paths, query parameter variations, error response details, webhook callbacks
Goal: a useful architecture overview a developer can understand in 30 seconds.`;
diff --git a/src/hooks/ai-generation/requestLifecycle.test.ts b/src/hooks/ai-generation/requestLifecycle.test.ts
index ec186326..732518ad 100644
--- a/src/hooks/ai-generation/requestLifecycle.test.ts
+++ b/src/hooks/ai-generation/requestLifecycle.test.ts
@@ -78,7 +78,7 @@ describe('requestLifecycle', () => {
await generateAIFlowResult({
chatMessages: [],
prompt: 'Enhance this repository diagram',
- seedDsl: 'flow: "Repository Module Structure"\ndirection: TB\n[section] api: API { color: "violet" }',
+ seedDsl: 'flow: "Repository Module Structure"\ndirection: TB\n[system] api: API { color: "violet", subLabel: "API layer" }',
nodes: [],
edges: [],
aiSettings: BASE_AI_SETTINGS,
diff --git a/src/hooks/ai-generation/sqlToErd.ts b/src/hooks/ai-generation/sqlToErd.ts
index ff4be799..29bfbb91 100644
--- a/src/hooks/ai-generation/sqlToErd.ts
+++ b/src/hooks/ai-generation/sqlToErd.ts
@@ -81,7 +81,7 @@ Rules:
- Create exactly one [entity] node per table — use the exact table name as the label
- List the key columns inside the node body (PK first, then FKs, then up to 3 other columns — omit the rest)
- Draw one directed edge per foreign key relationship; label it with the FK column and cardinality (1:1, 1:N, or N:M)
-- Group related tables into [section] containers where it makes semantic sense (e.g. "User Domain", "Commerce")
+- Do not use section or group containers; keep related tables adjacent and use labels to imply domains such as User or Commerce
- Do NOT invent tables, columns, or relationships that are not in the schema above
- Keep labels concise — table names as-is, column names as-is`;
}
diff --git a/src/hooks/ai-generation/streamingParser.ts b/src/hooks/ai-generation/streamingParser.ts
index 337bcf49..b49ec2b7 100644
--- a/src/hooks/ai-generation/streamingParser.ts
+++ b/src/hooks/ai-generation/streamingParser.ts
@@ -45,7 +45,7 @@ export function parseStreamingDsl(text: string): StreamingParseResult {
nodes.push({
id,
- type: nodeMatch[1] === 'section' ? 'section' : 'process',
+ type: nodeMatch[1] === 'section' ? 'group' : 'process',
position: { x: col * (NODE_W + GAP_X), y: row * (NODE_H + GAP_Y) },
data: { label },
width: NODE_W,
diff --git a/src/hooks/ai-generation/terraformToCloud.ts b/src/hooks/ai-generation/terraformToCloud.ts
index 8eade2e4..5bac70d4 100644
--- a/src/hooks/ai-generation/terraformToCloud.ts
+++ b/src/hooks/ai-generation/terraformToCloud.ts
@@ -6,17 +6,63 @@ export const TERRAFORM_FORMAT_LABELS: Record = {
'docker-compose': 'Docker Compose',
};
+const INFRA_ICON_HINTS = `AVAILABLE ICON PACKS (use these exact values when the resource matches):
+AWS (archProvider: "aws", archIconPackId: "aws-official-starter-v1"):
+ Lambda -> archIconShapeId: "compute-lambda", color: "violet"
+ EC2 -> archIconShapeId: "compute-ec2", color: "violet"
+ ECS/Fargate -> archIconShapeId: "containers-elastic-container-service", color: "violet"
+ EKS -> archIconShapeId: "containers-elastic-kubernetes-service", color: "violet"
+ API Gateway -> archIconShapeId: "app-integration-api-gateway", color: "violet"
+ S3 -> archIconShapeId: "storage-simple-storage-service", color: "emerald"
+ RDS/Postgres -> archIconShapeId: "database-rds", color: "emerald"
+ DynamoDB -> archIconShapeId: "database-dynamodb", color: "emerald"
+ ElastiCache -> archIconShapeId: "database-elasticache", color: "yellow"
+ SQS -> archIconShapeId: "app-integration-simple-queue-service", color: "amber"
+ SNS -> archIconShapeId: "app-integration-simple-notification-service", color: "amber"
+ CloudFront -> archIconShapeId: "networking-cloudfront", color: "pink"
+ Route 53 -> archIconShapeId: "networking-route-53", color: "pink"
+ Cognito -> archIconShapeId: "security-cognito", color: "slate"
+ CloudWatch -> archIconShapeId: "management-governance-cloudwatch", color: "slate"
+
+Azure (archProvider: "azure", archIconPackId: "azure-official-icons-v20"):
+ Azure Functions -> archIconShapeId: "compute-function-apps", color: "violet"
+ AKS -> archIconShapeId: "compute-kubernetes-services", color: "violet"
+ Azure SQL -> archIconShapeId: "databases-azure-sql", color: "emerald"
+ Blob Storage -> archIconShapeId: "storage-storage-accounts", color: "emerald"
+ Service Bus -> archIconShapeId: "integration-service-bus", color: "amber"
+ API Management -> archIconShapeId: "integration-api-management-services", color: "violet"
+
+GCP (archProvider: "gcp", archIconPackId: "gcp-official-icons-v1"):
+ Cloud Run -> archIconShapeId: "compute-cloud-run", color: "violet"
+ GKE -> archIconShapeId: "compute-kubernetes-engine", color: "violet"
+ Cloud SQL -> archIconShapeId: "databases-cloud-sql", color: "emerald"
+ Pub/Sub -> archIconShapeId: "data-analytics-pub-sub", color: "amber"
+ Cloud Storage -> archIconShapeId: "storage-cloud-storage", color: "emerald"
+
+CNCF/Kubernetes (archProvider: "cncf", archIconPackId: "cncf-artwork-icons-v1"):
+ Kubernetes -> archIconShapeId: "projects-clusternet", color: "violet"`;
+
export function buildTerraformToCloudPrompt(input: string, format: TerraformInputFormat): string {
const formatName = TERRAFORM_FORMAT_LABELS[format];
return `Analyze the following ${formatName} infrastructure-as-code and generate a cloud architecture diagram.
Guidelines:
-- Map each resource/service to a node using [system] for services, [section] for namespaces/VPCs/environments
-- Use cloud provider icon names where applicable (e.g. aws-s3, aws-rds, aws-ec2, gcp-gke, azure-vm)
+- Use [architecture] nodes for all infrastructure resources (databases, queues, storage, compute, networking)
+- Use [system] nodes for application services and containers
+- For each [architecture] node, set archProvider, archIconPackId, and archIconShapeId using the icon hints below — this ensures the correct provider icon is shown
- Show network topology: edges represent traffic flow, dependencies, or data movement
- Label edges with protocol or relationship (e.g. "HTTPS", "depends_on", "ingress")
-- Group resources by cloud region, VPC, or namespace using [section] containers
+- Do not use section or group containers; keep related resources adjacent and use labels or subtitles to communicate region, VPC, or namespace
- Surface key config like ports, instance types, or replica counts as node subtitles
+- Color by resource type:
+ - violet for compute (Lambda, EC2, ECS, Functions)
+ - emerald for databases and storage (RDS, S3, DynamoDB, Blob)
+ - amber for queues and messaging (SQS, SNS, Service Bus, Pub/Sub)
+ - yellow for caches (ElastiCache, Redis, Memorystore)
+ - pink for CDN and DNS (CloudFront, Route 53)
+ - slate for observability and IAM (CloudWatch, Cognito)
+
+${INFRA_ICON_HINTS}
${formatName.toUpperCase()}:
\`\`\`
diff --git a/src/hooks/flow-export/exportCapture.ts b/src/hooks/flow-export/exportCapture.ts
index a0413bb2..66c60386 100644
--- a/src/hooks/flow-export/exportCapture.ts
+++ b/src/hooks/flow-export/exportCapture.ts
@@ -48,6 +48,11 @@ export interface StreamingGifHandle {
finish(): Blob;
}
+interface VideoEncodeProgress {
+ completedFrames: number;
+ totalFrames: number;
+}
+
const EXPORT_CAPTURE_PADDING = 80;
const DEFAULT_EXPORT_WIDTH = 800;
const DEFAULT_EXPORT_HEIGHT = 600;
@@ -58,25 +63,67 @@ const EXPORT_FILTERED_CLASS_NAMES = [
'react-flow__background',
] as const;
-export function wait(ms: number): Promise {
- return new Promise((resolve) => {
- window.setTimeout(resolve, ms);
+export function createAbortError(): DOMException {
+ return new DOMException('The export was cancelled.', 'AbortError');
+}
+
+function throwIfAborted(signal?: AbortSignal): void {
+ if (signal?.aborted) {
+ throw createAbortError();
+ }
+}
+
+export function wait(ms: number, signal?: AbortSignal): Promise {
+ return new Promise((resolve, reject) => {
+ throwIfAborted(signal);
+ const timeoutId = window.setTimeout(() => {
+ cleanup();
+ resolve();
+ }, ms);
+
+ function handleAbort(): void {
+ cleanup();
+ reject(createAbortError());
+ }
+
+ function cleanup(): void {
+ window.clearTimeout(timeoutId);
+ signal?.removeEventListener('abort', handleAbort);
+ }
+
+ signal?.addEventListener('abort', handleAbort, { once: true });
});
}
-export function waitForAnimationFrame(): Promise {
- return new Promise((resolve) => {
- window.requestAnimationFrame(() => resolve());
+export function waitForAnimationFrame(signal?: AbortSignal): Promise {
+ return new Promise((resolve, reject) => {
+ throwIfAborted(signal);
+ const frameId = window.requestAnimationFrame(() => {
+ cleanup();
+ resolve();
+ });
+
+ function handleAbort(): void {
+ cleanup();
+ reject(createAbortError());
+ }
+
+ function cleanup(): void {
+ window.cancelAnimationFrame(frameId);
+ signal?.removeEventListener('abort', handleAbort);
+ }
+
+ signal?.addEventListener('abort', handleAbort, { once: true });
});
}
-export async function waitForExportRender(minDelayMs = 0): Promise {
+export async function waitForExportRender(minDelayMs = 0, signal?: AbortSignal): Promise {
if (minDelayMs > 0) {
- await wait(minDelayMs);
+ await wait(minDelayMs, signal);
}
- await waitForAnimationFrame();
- await waitForAnimationFrame();
+ await waitForAnimationFrame(signal);
+ await waitForAnimationFrame(signal);
}
export function createDownload(blob: Blob, fileName: string): void {
@@ -152,10 +199,15 @@ function loadImage(dataUrl: string): Promise {
});
}
-async function decodeCapturedFrame(dataUrl: string): Promise {
+async function decodeCapturedFrame(
+ dataUrl: string,
+ signal?: AbortSignal
+): Promise {
+ throwIfAborted(signal);
if (typeof window.createImageBitmap === 'function') {
const response = await fetch(dataUrl);
const blob = await response.blob();
+ throwIfAborted(signal);
return window.createImageBitmap(blob);
}
@@ -175,6 +227,13 @@ export async function decodeSingleFrame(dataUrl: string): Promise {
+ return decodeCapturedFrame(dataUrl, signal);
+}
+
function createExportCanvas(
width: number,
height: number
@@ -244,8 +303,20 @@ export async function encodeVideoFromFrames(params: {
mimeType: string;
backgroundColor?: string;
backgroundPainter?: FrameBackgroundPainter;
+ signal?: AbortSignal;
+ onProgress?: (progress: VideoEncodeProgress) => void;
}): Promise {
- const { frames, width, height, fps, mimeType, backgroundColor, backgroundPainter } = params;
+ const {
+ frames,
+ width,
+ height,
+ fps,
+ mimeType,
+ backgroundColor,
+ backgroundPainter,
+ signal,
+ onProgress,
+ } = params;
const { canvas, context } = createExportCanvas(width, height);
const stream = canvas.captureStream(fps);
const recorder = new MediaRecorder(stream, { mimeType });
@@ -263,17 +334,35 @@ export async function encodeVideoFromFrames(params: {
recorder.start();
const frameDurationMs = Math.max(1, Math.round(1000 / fps));
-
- for (const { frame, image } of frames) {
- const repeatCount = Math.max(1, Math.round(frame.delayMs / frameDurationMs));
- for (let repeatIndex = 0; repeatIndex < repeatCount; repeatIndex += 1) {
- renderDecodedFrame(context, width, height, image, backgroundColor, backgroundPainter);
- await wait(frameDurationMs);
+ const totalFrames = frames.reduce(
+ (sum, frameEntry) => sum + Math.max(1, Math.round(frameEntry.frame.delayMs / frameDurationMs)),
+ 0
+ );
+ let completedFrames = 0;
+
+ try {
+ for (const { frame, image } of frames) {
+ const repeatCount = Math.max(1, Math.round(frame.delayMs / frameDurationMs));
+ for (let repeatIndex = 0; repeatIndex < repeatCount; repeatIndex += 1) {
+ throwIfAborted(signal);
+ renderDecodedFrame(context, width, height, image, backgroundColor, backgroundPainter);
+ completedFrames += 1;
+ onProgress?.({ completedFrames, totalFrames });
+ await wait(frameDurationMs, signal);
+ }
+ }
+ } catch (error) {
+ if (recorder.state !== 'inactive') {
+ recorder.stop();
}
+ stream.getTracks().forEach((track) => track.stop());
+ throw error;
}
recorder.stop();
- return stopped;
+ const blob = await stopped;
+ stream.getTracks().forEach((track) => track.stop());
+ return blob;
}
export function createStreamingGifEncoder(
diff --git a/src/hooks/node-operations/useArchitectureNodeOperations.ts b/src/hooks/node-operations/useArchitectureNodeOperations.ts
index 7179a5ac..a87f51d1 100644
--- a/src/hooks/node-operations/useArchitectureNodeOperations.ts
+++ b/src/hooks/node-operations/useArchitectureNodeOperations.ts
@@ -1,13 +1,12 @@
import { useCallback } from 'react';
import type { FlowNode } from '@/lib/types';
-import { setNodeParent } from '@/lib/nodeParent';
import { useFlowStore } from '../../store';
import { createId } from '../../lib/id';
import { createDefaultEdge } from '@/constants';
import { assignSmartHandlesWithOptions, getSmartRoutingOptionsFromViewSettings } from '../../services/smartEdgeRouting';
import { buildArchitectureTemplate, type ArchitectureTemplateId } from '@/lib/architectureTemplates';
import { convertSelectedErNodesToClassDiagram } from '@/lib/erToClassConversion';
-import { createArchitectureServiceNode, createSectionNode, getAbsoluteNodePosition } from './utils';
+import { createArchitectureServiceNode } from './utils';
export const useArchitectureNodeOperations = (recordHistory: () => void) => {
const { setNodes, setEdges, setSelectedNodeId } = useFlowStore();
@@ -57,53 +56,9 @@ export const useArchitectureNodeOperations = (recordHistory: () => void) => {
}, [recordHistory, setNodes, setEdges, setSelectedNodeId]);
const handleCreateArchitectureBoundary = useCallback((sourceId: string): boolean => {
- const state = useFlowStore.getState();
- const sourceNode = state.nodes.find((node) => node.id === sourceId);
- if (!sourceNode || sourceNode.type !== 'architecture') {
- return false;
- }
- const sourceAbsolutePosition = getAbsoluteNodePosition(sourceNode, state.nodes);
-
- const boundaryId = createId('section');
- const { activeLayerId } = state;
- const boundaryLabel = `${sourceNode.data?.label || 'System'} Boundary`;
- const boundaryNode = createSectionNode(
- boundaryId,
- { x: sourceAbsolutePosition.x - 80, y: sourceAbsolutePosition.y - 70 },
- boundaryLabel
- );
- boundaryNode.style = { width: 360, height: 260 };
- boundaryNode.data = {
- ...boundaryNode.data,
- layerId: activeLayerId,
- archBoundaryId: boundaryId,
- };
-
- recordHistory();
- setNodes((existingNodes) => {
- const nextNodes: FlowNode[] = existingNodes.map((node) => {
- if (node.id === sourceId) {
- return setNodeParent({
- ...node,
- position: {
- x: sourceAbsolutePosition.x - boundaryNode.position.x,
- y: sourceAbsolutePosition.y - boundaryNode.position.y,
- },
- data: {
- ...node.data,
- archBoundaryId: boundaryId,
- },
- selected: true,
- }, boundaryId);
- }
- return { ...node, selected: false };
- });
- nextNodes.push({ ...boundaryNode, selected: false });
- return nextNodes;
- });
- setSelectedNodeId(sourceId);
- return true;
- }, [recordHistory, setNodes, setSelectedNodeId]);
+ void sourceId;
+ return false;
+ }, []);
const handleApplyArchitectureTemplate = useCallback((sourceId: string, templateId: ArchitectureTemplateId): boolean => {
const state = useFlowStore.getState();
diff --git a/src/hooks/useAIGeneration.ts b/src/hooks/useAIGeneration.ts
index 9f80506e..9d2c3c93 100644
--- a/src/hooks/useAIGeneration.ts
+++ b/src/hooks/useAIGeneration.ts
@@ -1,16 +1,29 @@
-import { useCallback, useEffect, useRef, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createLogger } from '@/lib/logger';
import type { FlowEdge, FlowNode } from '@/lib/types';
import { captureAnalyticsEvent } from '@/services/analytics/analytics';
-import type { ChatMessage } from '@/services/aiService';
+import { chatWithFlowpilot } from '@/services/aiService';
+import {
+ buildFlowpilotConversationPrompt,
+ buildFlowpilotAssistantSystemInstruction,
+ buildFlowpilotDiagramPrompt,
+} from '@/services/flowpilot/prompting';
+import { groundFlowpilotAssets, summarizeAssetGrounding } from '@/services/flowpilot/assetGrounding';
+import { buildFlowpilotPlan } from '@/services/flowpilot/responsePolicy';
+import {
+ assistantThreadToChatMessages,
+ createAnswerThreadItem,
+ createAppliedThreadItem,
+ createErrorThreadItem,
+ createPlanThreadItem,
+ createPreviewThreadItem,
+ createUserThreadItem,
+} from '@/services/flowpilot/thread';
+import type { AssistantThreadItem, AssetGroundingMatch } from '@/services/flowpilot/types';
import { useFlowStore } from '@/store';
import { useToast } from '@/components/ui/ToastContext';
import { toErrorMessage } from './ai-generation/graphComposer';
-import {
- appendChatExchange,
- generateAIFlowResult,
- type GenerateAIFlowResult,
-} from './ai-generation/requestLifecycle';
+import { generateAIFlowResult, type GenerateAIFlowResult } from './ai-generation/requestLifecycle';
import { parseStreamingDsl } from './ai-generation/streamingParser';
import { setStreamingGraph, setStreamingActive } from './ai-generation/streamingStore';
import {
@@ -29,8 +42,9 @@ import { buildOpenApiToSequencePrompt } from './ai-generation/openApiToSequence'
import { getAIReadinessState } from './ai-generation/readiness';
import {
clearChatHistory,
- loadChatHistory,
- saveChatHistory,
+ clearAssistantThreadHistory,
+ loadAssistantThreadHistory,
+ saveAssistantThreadHistory,
} from './ai-generation/chatHistoryStorage';
import { notifyOperationOutcome } from '@/services/operationFeedback';
@@ -43,6 +57,7 @@ export interface ImportDiff {
previewTitle: string;
previewDetail?: string;
previewStats?: string[];
+ assetMatches?: AssetGroundingMatch[];
result: GenerateAIFlowResult;
}
@@ -95,7 +110,8 @@ function computeImportDiff(
currentNodes: FlowNode[],
result: GenerateAIFlowResult,
requestKind: PreviewRequestKind,
- previewDescriptor?: PreviewDescriptor
+ previewDescriptor?: PreviewDescriptor,
+ assetMatches?: AssetGroundingMatch[]
): ImportDiff {
const currentIds = new Set(currentNodes.map((n) => n.id));
const newIds = new Set(result.layoutedNodes.map((n) => n.id));
@@ -107,6 +123,7 @@ function computeImportDiff(
addedCount,
removedCount,
updatedCount,
+ assetMatches,
...buildPreviewCopy(requestKind, addedCount, updatedCount, previewDescriptor),
result,
};
@@ -173,15 +190,37 @@ export function useAIGeneration(
const [streamingText, setStreamingText] = useState(null);
const [retryCount, setRetryCount] = useState(0);
const [pendingDiff, setPendingDiff] = useState(null);
+ const [assistantThread, setAssistantThread] = useState([]);
const abortControllerRef = useRef(null);
- const [chatMessages, setChatMessages] = useState([]);
+ const [lastError, setLastError] = useState(null);
+ const readiness = getAIReadinessState(aiSettings);
+
+ const chatMessages = useMemo(() => assistantThreadToChatMessages(assistantThread), [assistantThread]);
+
+ const persistThread = useCallback(
+ (nextItems: AssistantThreadItem[]) => {
+ void saveAssistantThreadHistory(activeTabId, nextItems);
+ },
+ [activeTabId]
+ );
+
+ const appendThreadItem = useCallback(
+ (item: AssistantThreadItem) => {
+ setAssistantThread((previous) => {
+ const next = [...previous, item];
+ persistThread(next);
+ return next;
+ });
+ },
+ [persistThread]
+ );
useEffect(() => {
let isDisposed = false;
- void loadChatHistory(activeTabId).then((messages) => {
+ void loadAssistantThreadHistory(activeTabId).then((messages) => {
if (!isDisposed) {
- setChatMessages(messages);
+ setAssistantThread(messages);
}
});
@@ -189,16 +228,15 @@ export function useAIGeneration(
isDisposed = true;
};
}, [activeTabId]);
- const [lastError, setLastError] = useState(null);
- const readiness = getAIReadinessState(aiSettings);
const cancelGeneration = useCallback(() => {
abortControllerRef.current?.abort();
}, []);
const clearChat = useCallback(() => {
+ void clearAssistantThreadHistory(activeTabId);
void clearChatHistory(activeTabId);
- setChatMessages([]);
+ setAssistantThread([]);
}, [activeTabId]);
const clearLastError = useCallback(() => {
@@ -209,15 +247,100 @@ export function useAIGeneration(
if (!pendingDiff) return;
recordHistory();
applyComposedGraph(pendingDiff.result.layoutedNodes, pendingDiff.result.layoutedEdges);
+ appendThreadItem(createAppliedThreadItem('Applied the preview to the canvas.'));
notifyOperationOutcome(addToast, { status: 'success', summary: 'Import applied to canvas.' });
setPendingDiff(null);
- }, [pendingDiff, applyComposedGraph, addToast, recordHistory]);
+ }, [pendingDiff, applyComposedGraph, addToast, recordHistory, appendThreadItem]);
const discardPendingDiff = useCallback(() => {
setPendingDiff(null);
}, []);
- const runAIRequest = useCallback(
+ const runConversationRequest = useCallback(
+ async (
+ prompt: string,
+ mode: 'answer' | 'plan',
+ assetMatches: AssetGroundingMatch[],
+ imageBase64?: string
+ ): Promise => {
+ if (!readiness.canGenerate && readiness.blockingIssue) {
+ setLastError(readiness.blockingIssue.detail);
+ return false;
+ }
+
+ setLastError(null);
+ setStreamingText('');
+ setRetryCount(0);
+ setIsGenerating(true);
+
+ const controller = new AbortController();
+ abortControllerRef.current = controller;
+
+ try {
+ const response = await chatWithFlowpilot(
+ chatMessages,
+ buildFlowpilotConversationPrompt(
+ prompt,
+ {
+ prompt,
+ nodeCount: nodes.length,
+ selectedNodeCount: selectedNodeIds.length,
+ hasImage: Boolean(imageBase64),
+ },
+ assetMatches,
+ mode
+ ),
+ buildFlowpilotAssistantSystemInstruction(mode),
+ aiSettings.apiKey,
+ aiSettings.model,
+ aiSettings.provider || 'gemini',
+ aiSettings.customBaseUrl,
+ (delta) => setStreamingText((previous) => (previous ?? '') + delta),
+ controller.signal
+ );
+
+ appendThreadItem(createAnswerThreadItem(response, mode, assetMatches));
+ notifyOperationOutcome(addToast, {
+ status: 'success',
+ summary: mode === 'plan' ? 'Plan ready.' : 'Flowpilot answered in chat.',
+ });
+ return true;
+ } catch (error) {
+ if (error instanceof DOMException && error.name === 'AbortError') {
+ return false;
+ }
+ const errorMessage = toErrorMessage(error);
+ setLastError(errorMessage);
+ appendThreadItem(createErrorThreadItem(errorMessage));
+ notifyOperationOutcome(addToast, {
+ status: 'error',
+ summary: 'Flowpilot could not finish the response.',
+ detail: errorMessage,
+ });
+ return false;
+ } finally {
+ abortControllerRef.current = null;
+ setIsGenerating(false);
+ setStreamingText(null);
+ setRetryCount(0);
+ }
+ },
+ [
+ addToast,
+ aiSettings.apiKey,
+ aiSettings.customBaseUrl,
+ aiSettings.model,
+ aiSettings.provider,
+ appendThreadItem,
+ chatMessages,
+ nodes.length,
+ readiness.blockingIssue,
+ readiness.canGenerate,
+ selectedNodeIds.length,
+ ]
+ );
+
+ const runDiagramRequest = useCallback(
async (
prompt: string,
imageBase64?: string,
@@ -225,7 +348,8 @@ export function useAIGeneration(
showPreview = false,
requestKind: PreviewRequestKind = 'prompt',
seedDsl?: string,
- previewDescriptor?: PreviewDescriptor
+ previewDescriptor?: PreviewDescriptor,
+ assetMatches?: AssetGroundingMatch[]
): Promise => {
if (!readiness.canGenerate && readiness.blockingIssue) {
setLastError(readiness.blockingIssue.detail);
@@ -258,7 +382,7 @@ export function useAIGeneration(
try {
const result = await generateAIFlowResult({
chatMessages,
- prompt,
+ prompt: buildFlowpilotDiagramPrompt(prompt, assetMatches ?? []),
seedDsl,
imageBase64,
nodes,
@@ -283,17 +407,25 @@ export function useAIGeneration(
signal: controller.signal,
});
- const { dslText, userMessage, layoutedNodes, layoutedEdges } = result;
- const isEditMode = nodes.length > 0 || Boolean(seedDsl);
- setChatMessages((previousMessages) => {
- const next = appendChatExchange(previousMessages, userMessage, dslText, isEditMode);
- void saveChatHistory(activeTabId, next);
- return next;
- });
-
+ const { dslText, layoutedNodes, layoutedEdges } = result;
if (showPreview) {
- const previewDiff = computeImportDiff(nodes, result, requestKind, previewDescriptor);
+ const previewDiff = computeImportDiff(
+ nodes,
+ result,
+ requestKind,
+ previewDescriptor,
+ assetMatches
+ );
setPendingDiff(previewDiff);
+ appendThreadItem(
+ createPreviewThreadItem(
+ dslText,
+ previewDiff.previewTitle,
+ previewDiff.previewDetail,
+ previewDiff.previewStats,
+ assetMatches
+ )
+ );
notifyOperationOutcome(addToast, {
status: 'success',
summary: previewDiff.previewTitle,
@@ -305,6 +437,7 @@ export function useAIGeneration(
});
} else {
applyComposedGraph(layoutedNodes, layoutedEdges);
+ appendThreadItem(createAppliedThreadItem(getSuccessSummary(nodes.length, focusedNodeIds)));
notifyOperationOutcome(addToast, {
status: 'success',
summary: getSuccessSummary(nodes.length, focusedNodeIds),
@@ -330,6 +463,7 @@ export function useAIGeneration(
const errorMessage = toErrorMessage(error);
logger.error('AI generation failed.', { error });
setLastError(errorMessage);
+ appendThreadItem(createErrorThreadItem(errorMessage));
captureAnalyticsEvent('ai_generation_failed', {
provider: aiSettings.provider || 'gemini',
is_preview: showPreview,
@@ -354,7 +488,7 @@ export function useAIGeneration(
[
addToast,
aiSettings,
- activeTabId,
+ appendThreadItem,
chatMessages,
edges,
globalEdgeOptions,
@@ -368,69 +502,124 @@ export function useAIGeneration(
const handleAIRequest = useCallback(
async (prompt: string, imageBase64?: string): Promise => {
- return runAIRequest(prompt, imageBase64, undefined, false, 'prompt');
+ const userThreadItem = createUserThreadItem(prompt, imageBase64);
+ appendThreadItem(userThreadItem);
+
+ const plan = buildFlowpilotPlan({
+ prompt,
+ nodeCount: nodes.length,
+ selectedNodeCount: selectedNodeIds.length,
+ hasImage: Boolean(imageBase64),
+ });
+ appendThreadItem(createPlanThreadItem(plan));
+
+ const assetMatches =
+ plan.mode === 'asset_suggestions' || plan.mode === 'diagram_preview'
+ ? await groundFlowpilotAssets(prompt)
+ : [];
+
+ if (plan.mode === 'asset_suggestions') {
+ const assetSummary = assetMatches.length > 0
+ ? `I found these strong local matches: ${summarizeAssetGrounding(assetMatches)}.`
+ : 'I could not find a strong local asset match yet. Try naming the cloud provider or exact service.';
+ appendThreadItem(createAnswerThreadItem(assetSummary, 'asset_suggestions', assetMatches));
+ return true;
+ }
+
+ if (plan.mode === 'answer' || plan.mode === 'plan' || plan.mode === 'clarification') {
+ return runConversationRequest(prompt, plan.mode === 'plan' ? 'plan' : 'answer', assetMatches, imageBase64);
+ }
+
+ return runDiagramRequest(prompt, imageBase64, undefined, true, 'prompt', undefined, undefined, assetMatches);
},
- [runAIRequest]
+ [
+ appendThreadItem,
+ nodes.length,
+ runConversationRequest,
+ runDiagramRequest,
+ selectedNodeIds.length,
+ ]
);
const handleFocusedAIRequest = useCallback(
async (prompt: string, focusedNodeIds: string[], imageBase64?: string): Promise => {
- return runAIRequest(prompt, imageBase64, focusedNodeIds, false, 'focused-edit');
+ appendThreadItem(createUserThreadItem(prompt, imageBase64));
+ appendThreadItem(
+ createPlanThreadItem(
+ buildFlowpilotPlan({
+ prompt,
+ nodeCount: nodes.length,
+ selectedNodeCount: focusedNodeIds.length,
+ hasImage: Boolean(imageBase64),
+ })
+ )
+ );
+ const assetMatches = await groundFlowpilotAssets(prompt);
+ return runDiagramRequest(
+ prompt,
+ imageBase64,
+ focusedNodeIds,
+ true,
+ 'focused-edit',
+ undefined,
+ undefined,
+ assetMatches
+ );
},
- [runAIRequest]
+ [appendThreadItem, nodes.length, runDiagramRequest]
);
const handleCodeAnalysis = useCallback(
async (code: string, language: SupportedLanguage): Promise => {
- return runAIRequest(
+ return runDiagramRequest(
buildCodeToArchitecturePrompt({ code, language }),
undefined,
undefined,
- true,
+ false,
'code-import'
);
},
- [runAIRequest]
+ [runDiagramRequest]
);
const handleSqlAnalysis = useCallback(
async (sql: string): Promise => {
- return runAIRequest(buildSqlToErdPrompt(sql), undefined, undefined, true, 'sql-import');
+ return runDiagramRequest(buildSqlToErdPrompt(sql), undefined, undefined, false, 'sql-import');
},
- [runAIRequest]
+ [runDiagramRequest]
);
const handleTerraformAnalysis = useCallback(
async (input: string, format: TerraformInputFormat): Promise => {
- return runAIRequest(
+ return runDiagramRequest(
buildTerraformToCloudPrompt(input, format),
undefined,
undefined,
- true,
+ false,
'terraform-import'
);
},
- [runAIRequest]
+ [runDiagramRequest]
);
const handleOpenApiAnalysis = useCallback(
async (spec: string): Promise => {
- return runAIRequest(
+ return runDiagramRequest(
buildOpenApiToSequencePrompt(spec),
undefined,
undefined,
- true,
+ false,
'openapi-import'
);
},
- [runAIRequest]
+ [runDiagramRequest]
);
const handleCodebaseAnalysis = useCallback(
async (analysis: CodebaseAnalysis): Promise => {
const nativeDiagram = buildCodebaseNativeDiagram(analysis);
- return runAIRequest(
+ return runDiagramRequest(
buildCodebaseToArchitecturePrompt({
summary: analysis.summary,
cloudPlatform: analysis.cloudPlatform,
@@ -446,7 +635,7 @@ export function useAIGeneration(
buildCodebasePreviewDescriptor(analysis, nativeDiagram)
);
},
- [runAIRequest]
+ [runDiagramRequest]
);
return {
@@ -467,6 +656,7 @@ export function useAIGeneration(
handleOpenApiAnalysis,
handleCodebaseAnalysis,
chatMessages,
+ assistantThread,
clearChat,
clearLastError,
};
diff --git a/src/hooks/useCinematicExport.ts b/src/hooks/useCinematicExport.ts
index 183dcd51..e5752943 100644
--- a/src/hooks/useCinematicExport.ts
+++ b/src/hooks/useCinematicExport.ts
@@ -1,6 +1,10 @@
import { useCallback } from 'react';
import { toPng } from 'html-to-image';
-import { useCinematicExportActions } from '@/context/CinematicExportContext';
+import {
+ useCinematicExportActions,
+ useCinematicExportJobState,
+} from '@/context/CinematicExportContext';
+import { useTheme } from '@/context/ThemeContext';
import { buildExportFileName } from '@/lib/exportFileName';
import { createLogger } from '@/lib/logger';
import type { FlowEdge, FlowNode } from '@/lib/types';
@@ -9,30 +13,35 @@ import {
selectSupportedVideoMimeType,
} from '@/services/animatedExport';
import {
- buildCinematicBuildPlan,
- type CinematicExportKind,
-} from '@/services/export/cinematicBuildPlan';
+ type CinematicExportRequest,
+} from '@/services/export/cinematicExport';
+import { buildCinematicBuildPlan } from '@/services/export/cinematicBuildPlan';
import {
buildCinematicTimeline,
getCinematicExportPreset,
resolveCinematicRenderState,
} from '@/services/export/cinematicRenderState';
import {
- CINEMATIC_EXPORT_FALLBACK_COLOR,
paintCinematicExportBackground,
+ resolveCinematicExportTheme,
} from '@/services/export/cinematicExportTheme';
import {
createDownload,
createExportOptions,
- createStreamingGifEncoder,
- decodeSingleFrame,
+ decodeSingleFrameWithSignal,
encodeVideoFromFrames,
waitForExportRender,
- type CapturedFrame,
} from './flow-export/exportCapture';
import { resolveFlowExportViewport } from './flowExportViewport';
const logger = createLogger({ scope: 'useCinematicExport' });
+const PREPARING_PROGRESS = 4;
+const CAPTURING_PROGRESS_START = 8;
+const CAPTURING_PROGRESS_END = 72;
+const ENCODING_PROGRESS_START = 76;
+const ENCODING_PROGRESS_END = 96;
+const FINALIZING_PROGRESS = 98;
+const RESET_DELAY_MS = 250;
interface AnimatedPlaybackControls {
stopPlayback: () => void;
@@ -47,6 +56,42 @@ interface UseCinematicExportParams {
exportBaseName: string | undefined;
}
+function isAbortError(error: unknown): boolean {
+ return error instanceof DOMException && error.name === 'AbortError';
+}
+
+function buildFrameTimes(totalDurationMs: number, frameDurationMs: number): number[] {
+ const frameTimes: number[] = [];
+ for (let timeMs = 0; timeMs < totalDurationMs; timeMs += frameDurationMs) {
+ frameTimes.push(timeMs);
+ }
+ return frameTimes;
+}
+
+function buildExportRequest(
+ request: CinematicExportRequest,
+ resolvedTheme: ReturnType['resolvedTheme']
+): CinematicExportRequest {
+ return {
+ ...request,
+ themeMode: request.themeMode ?? resolvedTheme,
+ };
+}
+
+function getProgressPercent(
+ completedFrames: number,
+ totalFrames: number,
+ start: number,
+ end: number
+): number {
+ if (totalFrames <= 0) {
+ return end;
+ }
+
+ const progress = completedFrames / totalFrames;
+ return Math.round(start + (end - start) * progress);
+}
+
export function useCinematicExport({
nodes,
edges,
@@ -55,17 +100,32 @@ export function useCinematicExport({
addToast,
exportBaseName,
}: UseCinematicExportParams): {
- handleCinematicExport: (kind: CinematicExportKind) => Promise;
+ handleCinematicExport: (request: CinematicExportRequest) => Promise;
} {
- const { setRenderState, resetRenderState } = useCinematicExportActions();
+ const { resolvedTheme } = useTheme();
+ const jobState = useCinematicExportJobState();
+ const {
+ setRenderState,
+ resetRenderState,
+ setJobState,
+ resetJobState,
+ registerCancelHandler,
+ } = useCinematicExportActions();
const handleCinematicExport = useCallback(
- async (kind: CinematicExportKind): Promise => {
+ async (incomingRequest: CinematicExportRequest): Promise => {
+ if (jobState.status !== 'idle') {
+ addToast('A cinematic export is already running.', 'info');
+ return;
+ }
+
if (!reactFlowWrapper.current) {
addToast('Canvas viewport not found.', 'error');
return;
}
+ const request = buildExportRequest(incomingRequest, resolvedTheme);
+
const { viewport: flowViewport, message } = resolveFlowExportViewport(
reactFlowWrapper.current
);
@@ -85,87 +145,95 @@ export function useCinematicExport({
return;
}
- const preset = getCinematicExportPreset(kind);
+ const preset = getCinematicExportPreset(request);
const timeline = buildCinematicTimeline(plan, preset);
+ const abortController = new AbortController();
+ const exportTheme = resolveCinematicExportTheme(request.themeMode);
+
+ if (request.resolution === '4k' && plan.segments.length > 80) {
+ addToast(
+ '4K cinematic export may take longer on larger diagrams. You can switch to 1080p for faster output.',
+ 'warning'
+ );
+ }
reactFlowWrapper.current.classList.add('exporting');
- addToast(
- kind === 'cinematic-gif' ? 'Rendering cinematic GIF…' : 'Rendering cinematic video…',
- 'info'
- );
+ registerCancelHandler(() => abortController.abort());
+ setJobState({
+ status: 'preparing',
+ progressPercent: PREPARING_PROGRESS,
+ completedFrames: 0,
+ totalFrames: 0,
+ stageLabel: 'Preparing cinematic export…',
+ canCancel: true,
+ request,
+ });
try {
- const frameOptions = createExportOptions(nodes, 'png', {
- maxDimension: timeline.preset.maxDimension,
- pixelRatio: timeline.preset.pixelRatio,
- }).options;
- const { width, height } = createExportOptions(nodes, 'png', {
+ const exportCapture = createExportOptions(nodes, 'png', {
maxDimension: timeline.preset.maxDimension,
pixelRatio: timeline.preset.pixelRatio,
});
const frameDurationMs = Math.max(1, Math.round(1000 / timeline.preset.fps));
+ const frameTimes = buildFrameTimes(timeline.totalDurationMs, frameDurationMs);
+ const totalCaptureFrames = frameTimes.length + 1;
animatedPlayback.stopPlayback();
const captureFrame = async (): Promise =>
toPng(flowViewport, {
- ...frameOptions,
- backgroundColor: CINEMATIC_EXPORT_FALLBACK_COLOR,
+ ...exportCapture.options,
+ backgroundColor: exportTheme.fallbackColor,
cacheBust: true,
});
- if (kind === 'cinematic-gif') {
- const gifEncoder = createStreamingGifEncoder(
- width,
- height,
- paintCinematicExportBackground
- );
-
- for (let timeMs = 0; timeMs < timeline.totalDurationMs; timeMs += frameDurationMs) {
- setRenderState(resolveCinematicRenderState(timeline, edges, timeMs));
- await waitForExportRender(8);
- const dataUrl = await captureFrame();
- await gifEncoder.addFrame(dataUrl, frameDurationMs);
- }
-
- setRenderState(resolveCinematicRenderState(timeline, edges, timeline.totalDurationMs));
- await waitForExportRender(8);
- const finalDataUrl = await captureFrame();
- await gifEncoder.addFrame(
- finalDataUrl,
- Math.max(frameDurationMs, timeline.preset.finalHoldMs)
- );
-
- const blob = gifEncoder.finish();
- createDownload(
- blob,
- buildExportFileName(exportBaseName ?? 'openflowkit-cinematic-build', 'gif')
- );
- addToast('Cinematic build GIF exported.', 'success');
- return;
- }
-
- const mimeType = selectSupportedVideoMimeType(window.MediaRecorder);
- if (!mimeType) {
- throw new Error(
- 'This browser does not support local video recording for cinematic export.'
- );
- }
+ const decodedFrames: Array<{
+ frame: { dataUrl: string; delayMs: number };
+ image: CanvasImageSource;
+ }> = [];
- const decodedFrames: Array<{ frame: CapturedFrame; image: CanvasImageSource }> = [];
+ setJobState((current) => ({
+ ...current,
+ status: 'capturing',
+ progressPercent: CAPTURING_PROGRESS_START,
+ totalFrames: totalCaptureFrames,
+ stageLabel: 'Capturing frames…',
+ }));
- for (let timeMs = 0; timeMs < timeline.totalDurationMs; timeMs += frameDurationMs) {
- setRenderState(resolveCinematicRenderState(timeline, edges, timeMs));
- await waitForExportRender(8);
+ for (const [frameIndex, timeMs] of frameTimes.entries()) {
+ setRenderState(resolveCinematicRenderState(timeline, edges, timeMs, request.themeMode));
+ await waitForExportRender(8, abortController.signal);
const dataUrl = await captureFrame();
- const image = await decodeSingleFrame(dataUrl);
+ const image = await decodeSingleFrameWithSignal(dataUrl, abortController.signal);
decodedFrames.push({ frame: { dataUrl, delayMs: frameDurationMs }, image });
+
+ const completedFrames = frameIndex + 1;
+ setJobState((current) => ({
+ ...current,
+ status: 'capturing',
+ completedFrames,
+ totalFrames: totalCaptureFrames,
+ stageLabel: 'Capturing frames…',
+ progressPercent: getProgressPercent(
+ completedFrames,
+ totalCaptureFrames,
+ CAPTURING_PROGRESS_START,
+ CAPTURING_PROGRESS_END
+ ),
+ }));
}
- setRenderState(resolveCinematicRenderState(timeline, edges, timeline.totalDurationMs));
- await waitForExportRender(8);
+ setRenderState(
+ resolveCinematicRenderState(
+ timeline,
+ edges,
+ timeline.totalDurationMs,
+ request.themeMode
+ )
+ );
+ await waitForExportRender(8, abortController.signal);
const finalUrl = await captureFrame();
- const finalImage = await decodeSingleFrame(finalUrl);
+ const finalImage = await decodeSingleFrameWithSignal(finalUrl, abortController.signal);
decodedFrames.push({
frame: {
dataUrl: finalUrl,
@@ -174,27 +242,96 @@ export function useCinematicExport({
image: finalImage,
});
+ setJobState((current) => ({
+ ...current,
+ status: 'encoding',
+ completedFrames: 0,
+ totalFrames: Math.max(1, decodedFrames.length),
+ stageLabel: 'Encoding video…',
+ progressPercent: ENCODING_PROGRESS_START,
+ }));
+
+ const mimeType = selectSupportedVideoMimeType(window.MediaRecorder);
+ if (!mimeType) {
+ throw new Error(
+ 'This browser does not support local video recording for cinematic export.'
+ );
+ }
+
const blob = await encodeVideoFromFrames({
frames: decodedFrames,
- width,
- height,
+ width: exportCapture.width,
+ height: exportCapture.height,
fps: timeline.preset.fps,
mimeType,
- backgroundPainter: paintCinematicExportBackground,
+ signal: abortController.signal,
+ backgroundPainter: (context, width, height) =>
+ paintCinematicExportBackground(context, width, height, request.themeMode),
+ onProgress: ({ completedFrames, totalFrames }) => {
+ setJobState((current) => ({
+ ...current,
+ status: 'encoding',
+ completedFrames,
+ totalFrames,
+ stageLabel: 'Encoding video…',
+ progressPercent: getProgressPercent(
+ completedFrames,
+ totalFrames,
+ ENCODING_PROGRESS_START,
+ ENCODING_PROGRESS_END
+ ),
+ }));
+ },
});
+
+ setJobState((current) => ({
+ ...current,
+ status: 'finalizing',
+ progressPercent: FINALIZING_PROGRESS,
+ stageLabel: 'Finalizing export…',
+ }));
+
const extension = getAnimatedExportFileExtension(mimeType);
createDownload(
blob,
buildExportFileName(exportBaseName ?? 'openflowkit-cinematic-build', extension)
);
+ setJobState((current) => ({
+ ...current,
+ status: 'done',
+ progressPercent: 100,
+ canCancel: false,
+ stageLabel: 'Export complete',
+ }));
addToast(`Cinematic build ${extension.toUpperCase()} exported.`, 'success');
} catch (error) {
+ if (isAbortError(error)) {
+ setJobState((current) => ({
+ ...current,
+ status: 'cancelled',
+ canCancel: false,
+ stageLabel: 'Export cancelled',
+ }));
+ addToast('Cinematic export cancelled.', 'info');
+ return;
+ }
+
const exportMessage = error instanceof Error ? error.message : 'Cinematic export failed.';
- logger.error('Cinematic export failed.', { error, kind });
+ logger.error('Cinematic export failed.', { error, request });
+ setJobState((current) => ({
+ ...current,
+ status: 'error',
+ canCancel: false,
+ stageLabel: exportMessage,
+ }));
addToast(exportMessage, 'error');
} finally {
resetRenderState();
+ registerCancelHandler(null);
reactFlowWrapper.current?.classList.remove('exporting');
+ window.setTimeout(() => {
+ resetJobState();
+ }, RESET_DELAY_MS);
}
},
[
@@ -202,9 +339,14 @@ export function useCinematicExport({
animatedPlayback,
edges,
exportBaseName,
+ jobState.status,
nodes,
reactFlowWrapper,
+ registerCancelHandler,
+ resetJobState,
resetRenderState,
+ resolvedTheme,
+ setJobState,
setRenderState,
]
);
diff --git a/src/hooks/useFlowEditorUIState.ts b/src/hooks/useFlowEditorUIState.ts
index 5e70733e..d3ff84d6 100644
--- a/src/hooks/useFlowEditorUIState.ts
+++ b/src/hooks/useFlowEditorUIState.ts
@@ -8,7 +8,7 @@ export type CommandBarView =
| 'assets';
export type FlowEditorMode = 'canvas' | 'studio';
-export type StudioTab = 'ai' | 'code' | 'playback' | 'infra' | 'lint';
+export type StudioTab = 'ai' | 'code' | 'playback' | 'infra';
export type StudioCodeMode = 'openflow' | 'mermaid';
interface UseFlowEditorUIStateResult {
@@ -20,11 +20,14 @@ interface UseFlowEditorUIStateResult {
studioCodeMode: StudioCodeMode;
isSelectMode: boolean;
isDesignSystemPanelOpen: boolean;
+ isArchitectureRulesOpen: boolean;
openHistory: () => void;
closeHistory: () => void;
openCommandBar: (view?: CommandBarView) => void;
closeCommandBar: () => void;
openDesignSystemPanel: () => void;
+ openArchitectureRulesPanel: () => void;
+ closeArchitectureRulesPanel: () => void;
setCanvasMode: () => void;
setStudioMode: () => void;
setStudioTab: (tab: StudioTab) => void;
@@ -41,6 +44,7 @@ export function useFlowEditorUIState(): UseFlowEditorUIStateResult {
const [studioTab, setStudioTab] = useState('ai');
const [studioCodeMode, setStudioCodeMode] = useState('openflow');
const [isSelectMode, setIsSelectMode] = useState(true);
+ const [isArchitectureRulesOpen, setIsArchitectureRulesOpen] = useState(false);
function openHistory(): void {
setIsHistoryOpen(true);
@@ -64,12 +68,22 @@ export function useFlowEditorUIState(): UseFlowEditorUIStateResult {
setIsCommandBarOpen(true);
}
+ function openArchitectureRulesPanel(): void {
+ setIsArchitectureRulesOpen(true);
+ }
+
+ function closeArchitectureRulesPanel(): void {
+ setIsArchitectureRulesOpen(false);
+ }
+
function setCanvasMode(): void {
setEditorMode('canvas');
+ setIsArchitectureRulesOpen(false);
}
function setStudioMode(): void {
setEditorMode('studio');
+ setIsArchitectureRulesOpen(false);
}
function enableSelectMode(): void {
@@ -89,11 +103,14 @@ export function useFlowEditorUIState(): UseFlowEditorUIStateResult {
studioCodeMode,
isSelectMode,
isDesignSystemPanelOpen: isCommandBarOpen && commandBarView === 'design-system',
+ isArchitectureRulesOpen,
openHistory,
closeHistory,
openCommandBar,
closeCommandBar,
openDesignSystemPanel,
+ openArchitectureRulesPanel,
+ closeArchitectureRulesPanel,
setCanvasMode,
setStudioMode,
setStudioTab,
diff --git a/src/hooks/useLayoutOperations.ts b/src/hooks/useLayoutOperations.ts
index 31de1edc..f12095c6 100644
--- a/src/hooks/useLayoutOperations.ts
+++ b/src/hooks/useLayoutOperations.ts
@@ -1,13 +1,8 @@
import { useCallback } from 'react';
-import type { FlowNode } from '@/lib/types';
import { useFlowStore } from '../store';
import { alignNodes, distributeNodes } from '../services/AlignDistribute';
-import { useTranslation } from 'react-i18next';
-import { createId } from '@/lib/id';
-import { createSectionNode } from './node-operations/nodeFactories';
export const useLayoutOperations = (recordHistory: () => void) => {
- const { t } = useTranslation();
const { setNodes } = useFlowStore();
const handleAlignNodes = useCallback((direction: 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom') => {
@@ -36,71 +31,9 @@ export const useLayoutOperations = (recordHistory: () => void) => {
}));
}, [recordHistory, setNodes]);
- const handleGroupNodes = useCallback(() => {
- const { nodes } = useFlowStore.getState();
- const selectedNodes = nodes.filter(n => n.selected);
- if (selectedNodes.length < 2) return;
-
- recordHistory();
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
- selectedNodes.forEach(n => {
- minX = Math.min(minX, n.position.x);
- minY = Math.min(minY, n.position.y);
- maxX = Math.max(maxX, n.position.x + (n.width || 150));
- maxY = Math.max(maxY, n.position.y + (n.height || 40));
- });
-
- const padding = 40;
- const groupNode: FlowNode = {
- id: createId('group'),
- type: 'group',
- position: { x: minX - padding, y: minY - padding },
- style: { width: maxX - minX + padding * 2, height: maxY - minY + padding * 2 },
- data: { label: t('nodes.group'), subLabel: `${selectedNodes.length} ${t('nodes.items')}` },
- selected: true,
- zIndex: -1
- };
-
- setNodes((nds) => [groupNode, ...nds]);
- }, [recordHistory, setNodes, t]);
-
- const handleWrapInSection = useCallback(() => {
- const { nodes } = useFlowStore.getState();
- const selectedNodes = nodes.filter(n => n.selected && n.type !== 'section');
- if (selectedNodes.length < 1) return;
-
- recordHistory();
- const padding = 48;
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
- selectedNodes.forEach(n => {
- minX = Math.min(minX, n.position.x);
- minY = Math.min(minY, n.position.y);
- maxX = Math.max(maxX, n.position.x + (n.width || 150));
- maxY = Math.max(maxY, n.position.y + (n.height || 40));
- });
-
- const sectionId = createId('section');
- const sectionX = minX - padding;
- const sectionY = minY - padding - 44; // 44px for title bar
- const sectionW = maxX - minX + padding * 2;
- const sectionH = maxY - minY + padding * 2 + 44;
-
- const sectionNode = createSectionNode(sectionId, { x: sectionX, y: sectionY }, t('nodes.section', 'Section'));
- const styledSection: FlowNode = {
- ...sectionNode,
- style: { width: sectionW, height: sectionH },
- selected: true,
- };
+ const handleGroupNodes = useCallback(() => {}, []);
- setNodes((nds) => {
- const deselected = nds.map(n =>
- selectedNodes.find(s => s.id === n.id)
- ? { ...n, parentId: sectionId, position: { x: n.position.x - sectionX, y: n.position.y - sectionY }, selected: false, extent: 'parent' as const }
- : { ...n, selected: false }
- );
- return [styledSection, ...deselected];
- });
- }, [recordHistory, setNodes, t]);
+ const handleWrapInSection = useCallback(() => {}, []);
return {
handleAlignNodes,
diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json
index 2a425cc5..064ff06d 100644
--- a/src/i18n/locales/de/translation.json
+++ b/src/i18n/locales/de/translation.json
@@ -67,7 +67,10 @@
"distributeVertically": "Vertikal verteilen",
"group": "Gruppe",
"upload": "Hochladen",
- "rename": "Umbenennen"
+ "rename": "Umbenennen",
+ "export": "Exportieren",
+ "exportDiagram": "Diagramm exportieren",
+ "exportAs": "Exportieren als"
},
"nav": {
"home": "Startseite",
@@ -124,12 +127,12 @@
"openflowdsl": "Als OpenFlow DSL exportieren",
"figma": "Nach Figma exportieren",
"hintTransparent4K": "Transparent (4K)",
- "hintWhiteBg4K": "White Background (4K)",
+ "hintWhiteBg4K": "Weißer Hintergrund (4K)",
"jsonLabel": "JSON File",
- "hintDownload": "Download",
+ "hintDownload": "Herunterladen",
"openflowdslLabel": "{{appName}} DSL",
"hintClipboard": "Kopieren",
- "figmaEditable": "Figma Editable",
+ "figmaEditable": "Figma-bearbeitbar",
"exportDiagram": "Export Diagram",
"exportAs": "Export As",
"actionCopy": "Kopieren",
@@ -144,27 +147,28 @@
"shareEmbed": "Teilen & Einbetten",
"hintShareViewer": "Einladungslink für diesen Raum",
"hintShareEmbed": "Nur-Lese-Link für Viewer",
- "readmeEmbed": "README Embed",
- "hintReadmeEmbed": "Copy Markdown snippet",
- "sectionImage": "Image",
+ "readmeEmbed": "README-Einbettung",
+ "hintReadmeEmbed": "Markdown-Snippet kopieren",
+ "sectionImage": "Bild",
"sectionVideo": "Video",
"sectionCode": "Code",
"shareSection": "Canvas teilen",
- "revealVideo": "Reveal Video",
- "hintRevealVideo": "Nodes fade in sequentially (WebM/MP4)",
- "revealGif": "Reveal GIF",
- "hintRevealGif": "Animated reveal for docs/social",
- "cinematicVideo": "Cinematic Build Video",
+ "revealVideo": "Video anzeigen",
+ "hintRevealVideo": "Knoten werden nacheinander eingeblendet (WebM/MP4)",
+ "revealGif": "GIF anzeigen",
+ "hintRevealGif": "Animierte Aufdeckung für Doku/Social",
+ "cinematicVideo": "Cinematic-Build-Video",
"hintCinematicVideo": "Dark launch-style build",
- "cinematicGif": "Cinematic Build GIF",
- "hintCinematicGif": "Dark social-ready loop",
- "actionDownload": "Download",
+ "cinematicGif": "Cinematic-Build-GIF",
+ "hintCinematicGif": "Dunkle Social-Schleife",
+ "actionDownload": "Herunterladen",
"hintDocument": "Dokument",
"hintEditableSvg": "Bearbeitbares SVG",
"chooseFormat": "Format wählen",
"speed": "Geschwindigkeit",
"resolution": "Auflösung",
- "transparentBackground": "Transparenter Hintergrund"
+ "transparentBackground": "Transparenter Hintergrund",
+ "headingFormat": "Exportformat"
},
"import": {
"title": "Importieren",
@@ -267,26 +271,26 @@
"hint": "Dies kann nicht rückgängig gemacht werden, es sei denn, Sie haben ein exportiertes Backup oder eine weitere Kopie.",
"closeDialog": "Dialog zum Löschen des Flows schließen"
},
+ "homeEmptyTitle": "Erstellen Sie Ihren ersten Flow",
+ "homeEmptySubtitle": "Entwerfen Sie sofort Enterprise-Architekturen. Beginnen Sie mit einer leeren Leinwand, beschreiben Sie Ihre Infrastruktur mit unserem KI-Builder oder verwenden Sie eine maßgeschneiderte Vorlage.",
+ "homeBlankCanvas": "Leere Leinwand",
+ "homeFlowpilotAI": "Flowpilot KI",
+ "homeTemplates": "Vorlagen",
+ "homeImportFile": "Oder eine bestehende Datei importieren",
+ "suggestionAI": "Flowpilot KI",
"suggestionBlank": "Leere Leinwand",
"suggestionBlankDesc": "Direkt in den Editor einsteigen",
- "suggestionAI": "Flowpilot KI",
"suggestionAIDesc": "Mit einer Eingabe beginnen",
"suggestionImport": "Importieren",
"suggestionImportDesc": "Bestehende Arbeit einbringen",
"suggestionTemplates": "Vorlagen",
"suggestionTemplatesDesc": "Mit einem bewährten Muster beginnen",
- "continueTitle": "Mit einer kürzlichen Aktion fortfahren",
- "homeEmptyTitle": "Erstellen Sie Ihren ersten Flow",
- "homeEmptySubtitle": "Entwerfen Sie sofort Enterprise-Architekturen. Beginnen Sie mit einer leeren Leinwand, beschreiben Sie Ihre Infrastruktur mit unserem KI-Builder oder verwenden Sie eine maßgeschneiderte Vorlage.",
- "homeBlankCanvas": "Leere Leinwand",
- "homeFlowpilotAI": "Flowpilot KI",
- "homeTemplates": "Vorlagen",
- "homeImportFile": "Oder eine bestehende Datei importieren"
+ "continueTitle": "Mit einer kürzlichen Aktion fortfahren"
},
"settings": {
"title": "Einstellungen",
"general": "Allgemein",
- "canvas": "Canvas",
+ "canvas": "Leinwand",
"shortcuts": "Tastaturkürzel",
"theme": "Design",
"language": "Sprache",
@@ -298,14 +302,14 @@
"about": "Über",
"brand": "Markeneinstellungen",
"privacy": "Datenschutz",
- "themeLight": "Light",
- "themeDark": "Dark",
+ "themeLight": "Hell",
+ "themeDark": "Dunkel",
"themeSystem": "System"
},
"shortcuts": {
"title": "Tastaturkürzel",
"general": "Allgemein",
- "canvas": "Canvas",
+ "canvas": "Leinwand",
"nodes": "Knoten",
"essentials": "Grundlagen",
"manipulation": "Bearbeitung",
@@ -313,28 +317,28 @@
"help": "Hilfe"
},
"properties": {
- "title": "Properties",
- "shape": "Shape",
- "color": "Color",
- "icon": "Icon",
- "rotation": "Rotation",
- "transparency": "Transparency",
- "imageSettings": "Image Settings",
- "bulkShape": "Bulk Shape",
- "bulkColor": "Bulk Color",
- "bulkIcon": "Bulk Icon",
- "labelTransform": "Label Transform",
- "findReplace": "Find & Replace",
- "findLabel": "Find",
- "findPlaceholder": "Find text",
- "replaceLabel": "Replace",
- "replacePlaceholder": "Replace with",
- "prefixOptional": "Prefix (optional)",
+ "title": "Eigenschaften",
+ "shape": "Form",
+ "color": "Farbe",
+ "icon": "Symbol",
+ "rotation": "Drehung",
+ "transparency": "Transparenz",
+ "imageSettings": "Bildeinstellungen",
+ "bulkShape": "Form (Mehrfachauswahl)",
+ "bulkColor": "Farbe (Mehrfachauswahl)",
+ "bulkIcon": "Symbol (Mehrfachauswahl)",
+ "labelTransform": "Beschriftungstransformation",
+ "findReplace": "Suchen & Ersetzen",
+ "findLabel": "Suchen",
+ "findPlaceholder": "Text suchen",
+ "replaceLabel": "Ersetzen",
+ "replacePlaceholder": "Ersetzen durch",
+ "prefixOptional": "Präfix (optional)",
"suffixOptional": "Suffix (optional)",
- "useRegex": "Use Regex",
- "applyToSelectedNodes": "Apply to selected nodes",
- "selectFieldToApply": "Select a field to apply",
- "previewSummary": "Preview summary",
+ "useRegex": "Regex verwenden",
+ "applyToSelectedNodes": "Auf ausgewählte Knoten anwenden",
+ "selectFieldToApply": "Feld zum Anwenden auswählen",
+ "previewSummary": "Vorschau-Zusammenfassung",
"selectionSummary": "Auswahlübersicht"
},
"saveStatus": {
@@ -346,8 +350,14 @@
"model": "Modell",
"apiKey": "API-Schlüssel",
"placeholder": "Flussdiagramm beschreiben...",
- "settingsSubtitle": "Configure your preferred AI provider, model, and API key below.",
- "generateWithFlowpilot": "Mit Flowpilot generieren"
+ "settingsSubtitle": "Konfigurieren Sie unten Ihren bevorzugten KI-Anbieter, das Modell und den API-Schlüssel.",
+ "generateWithFlowpilot": "Mit Flowpilot generieren",
+ "howToGetKey": "So erhalten Sie Ihren API-Schlüssel",
+ "openConsole": "Konsole öffnen",
+ "pasteKeyStep": "Fügen Sie ihn oben ein — er wird niemals mit uns geteilt",
+ "customEndpointTitle": "Was ist ein benutzerdefinierter Endpunkt?",
+ "customModelHint": "Geben Sie die genaue Modell-ID für Ihren Endpunkt ein",
+ "privacyTitle": "Datenschutz & Verschlüsselung"
},
"playback": {
"title": "Wiedergabe",
@@ -615,6 +625,13 @@
"edgeSyntax": "Verwenden Sie Architektur-Kantenpfeile `-->`, `<--` oder `<-->` sowie Seitenqualifikatoren wie `api:R --> L:db`.",
"nodeSyntax": "Verwenden Sie gültige Knotendeklarationen: `service id(icon)[Label]`, `group id[Label]`, `junction id[Label]`.",
"fallback": "Deaktivieren Sie den Architektur-Strict-Mode, um automatische Wiederherstellung zuzulassen, oder beheben Sie die Diagnosen und versuchen Sie es erneut."
+ },
+ "jumpToLine": "Springe zu Zeile {{line}}",
+ "diagnosticsGroup": {
+ "syntax": "Syntax-Probleme",
+ "identity": "Bezeichner-Probleme",
+ "recovery": "Wiederherstellungswarnungen",
+ "general": "Diagnosen"
}
},
"layout": {
@@ -757,9 +774,7 @@
"infra": "Infra",
"openapi": "OpenAPI",
"code": "Code",
- "codebase": "Repository",
- "mindmap": "Mindmap",
- "markdown": "Markdown"
+ "codebase": "Repository"
},
"title": "Aus Daten importieren",
"description": "Importiere Code, Infrastruktur, SQL oder API-Spezifikationen in dein Diagramm.",
@@ -838,7 +853,9 @@
"edgeBundling": "Geschwisterkanten bündeln",
"edgeBundlingDesc": "Parallele Verbindungen auf gemeinsamen Spuren halten",
"architectureStrictMode": "Architektur-Strict-Modus",
- "architectureStrictModeDesc": "Mermaid-Import blockieren, wenn Architekturdiagnosen Wiederherstellungs-/Validierungsprobleme enthalten"
+ "architectureStrictModeDesc": "Mermaid-Import blockieren, wenn Architekturdiagnosen Wiederherstellungs-/Validierungsprobleme enthalten",
+ "miniMap": "Minimap",
+ "miniMapDesc": "Minimap unten rechts anzeigen"
},
"ai": {
"provider": "Anbieter",
@@ -912,132 +929,227 @@
},
"models": {
"gemini": {
- "gemini-2": {
- "5-flash-lite": {
- "label": "Gemini 2.5 Flash Lite"
- },
- "5-flash": {
- "label": "Gemini 2.5 Flash"
- },
- "5-pro": {
- "label": "Gemini 2.5 Pro"
- }
- },
"gemini-3-flash": {
- "label": "Gemini 3 Flash"
+ "label": "Gemini 3 Flash",
+ "hint": "Frontier speed + intelligence",
+ "category": "Legacy",
+ "badge": "New"
},
"gemini-3-pro": {
- "label": "Gemini 3 Pro"
+ "label": "Gemini 3 Pro",
+ "hint": "Most powerful · Multimodal",
+ "category": "Legacy",
+ "badge": "New"
+ },
+ "gemini-2.5-flash-lite": {
+ "label": "2.5 Flash Lite",
+ "hint": "Fastest · Free tier default",
+ "category": "Speed",
+ "badge": "Default"
+ },
+ "gemini-2.5-flash": {
+ "label": "2.5 Flash",
+ "hint": "Best price/performance balance",
+ "category": "Speed"
+ },
+ "gemini-2.5-pro": {
+ "label": "2.5 Pro",
+ "hint": "Best reasoning · Complex diagrams",
+ "category": "Reasoning"
}
},
"openai": {
"gpt-5-mini": {
- "label": "GPT-5 Mini"
- },
- "gpt-5": {
- "2": {
- "label": "GPT-5.2"
- },
- "label": "GPT-5"
+ "label": "GPT-5 Mini",
+ "hint": "Fast · Cost-efficient",
+ "category": "Speed",
+ "badge": "Default"
},
"o4-mini": {
- "label": "O4-Mini"
+ "label": "O4-Mini",
+ "hint": "Advanced reasoning · Fast",
+ "category": "Reasoning",
+ "badge": "Reasoning"
},
"o3": {
- "label": "O3"
+ "label": "O3",
+ "hint": "Deep reasoning · Complex tasks",
+ "category": "Reasoning",
+ "badge": "Reasoning"
+ },
+ "gpt-5": {
+ "label": "GPT-5",
+ "hint": "Flagship model · Most capable",
+ "category": "Flagship"
+ },
+ "gpt-5.2": {
+ "label": "GPT-5.2",
+ "hint": "Latest update · Improved reasoning",
+ "category": "Reasoning",
+ "badge": "New"
}
},
"claude": {
"claude-haiku-4-5": {
- "label": "Claude Haiku 4.5"
+ "label": "Claude Haiku 4.5",
+ "hint": "Fastest · Most affordable",
+ "category": "Speed"
},
"claude-sonnet-4-5": {
- "label": "Claude Sonnet 4.5"
+ "label": "Claude Sonnet 4.5",
+ "hint": "Balanced intelligence & speed",
+ "category": "Flagship"
},
"claude-sonnet-4-6": {
- "label": "Claude Sonnet 4.6"
+ "label": "Claude Sonnet 4.6",
+ "hint": "Latest Sonnet · Best coding",
+ "category": "Flagship",
+ "badge": "Default"
},
"claude-opus-4-6": {
- "label": "Claude Opus 4.6"
+ "label": "Claude Opus 4.6",
+ "hint": "Most intelligent · 1M token context",
+ "category": "Reasoning",
+ "badge": "Flagship"
}
},
"groq": {
"meta-llama/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Free tier · Very fast",
+ "category": "Speed",
+ "badge": "Free"
},
"meta-llama/llama-4-maverick-17b-128e-instruct": {
- "label": "Llama 4 Maverick"
+ "label": "Llama 4 Maverick",
+ "hint": "More capable · Free tier",
+ "category": "Speed",
+ "badge": "Free"
},
"qwen/qwen3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
},
- "llama-3": {
- "3-70b-versatile": {
- "label": "Llama 3.3 70B"
- }
+ "llama-3.3-70b-versatile": {
+ "label": "Llama 3.3 70B Versatile",
+ "hint": "Versatile model",
+ "category": "Performance",
+ "badge": "Performance"
}
},
"nvidia": {
"meta/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Efficient · Multi-modal",
+ "category": "Speed"
},
"nvidia/nemotron-nano-12b-v2-vl": {
- "label": "Nemotron Nano 12B"
+ "label": "Nemotron Nano 12B",
+ "hint": "Lightweight · Vision-language · Fast",
+ "category": "Speed"
},
"deepseek/deepseek-v3-2": {
- "label": "DeepSeek V3-2"
+ "label": "DeepSeek V3-2",
+ "hint": "Latest · GPT-5 comparable",
+ "category": "Flagship",
+ "badge": "New"
},
"qwen/qwq-32b": {
- "label": "QwQ 32B"
+ "label": "QwQ 32B",
+ "hint": "Strong reasoning model",
+ "category": "Reasoning"
},
"moonshotai/kimi-k2-thinking": {
- "label": "Kimi K2 Thinking"
+ "label": "Kimi K2 Thinking",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"cerebras": {
"gpt-oss-120b": {
- "label": "GPT OSS 120B"
+ "label": "GPT OSS 120B",
+ "hint": "120B params · Fast on WSE-3",
+ "category": "Speed",
+ "badge": "Default"
},
"qwen-3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "2,403 tok/s · Industry fastest",
+ "category": "Speed",
+ "badge": "🚀 Fastest"
},
"qwen-3-235b-a22b": {
- "label": "Qwen 3 235B"
+ "label": "Qwen 3 235B",
+ "hint": "Flagship · Best quality",
+ "category": "Flagship",
+ "badge": "Flagship"
},
- "zai-glm-4": {
- "7": {
- "label": "Zai GLM 4.7"
- }
+ "zai-glm-4.7": {
+ "label": "Zai-GLM 4.7",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"mistral": {
"mistral-small-latest": {
- "label": "Mistral Small"
+ "label": "Mistral Small",
+ "hint": "Fast · Cost-efficient · 32k context",
+ "category": "Speed",
+ "badge": "Free"
},
"mistral-medium-latest": {
- "label": "Mistral Medium"
+ "label": "Mistral Medium",
+ "hint": "Balanced quality-cost · Best default",
+ "category": "Flagship",
+ "badge": "Default"
},
"mistral-large-latest": {
- "label": "Mistral Large"
+ "label": "Mistral Large",
+ "hint": "Most capable · 128k context · Flagship",
+ "category": "Flagship",
+ "badge": "Flagship"
},
"codestral-latest": {
- "label": "Codestral"
+ "label": "Codestral",
+ "hint": "Code-optimized · 256k context",
+ "category": "Coding",
+ "badge": "Code"
},
"pixtral-large-latest": {
- "label": "Pixtral Large"
+ "label": "Pixtral Large",
+ "hint": "Vision + reasoning · Multimodal",
+ "category": "Multimodal",
+ "badge": "Vision"
+ }
+ },
+ "custom": {
+ "custom": {
+ "label": "Custom Model",
+ "hint": "Enter your model ID below",
+ "category": "Custom"
}
}
},
"customEndpoints": {
"ollama": {
- "name": "Ollama"
+ "name": "Ollama",
+ "hint": "Local · Free"
},
"lmStudio": {
- "name": "LM Studio"
+ "name": "LM Studio",
+ "hint": "Local · Free"
},
"together": {
- "name": "Together AI"
+ "name": "Together AI",
+ "hint": "Cloud · Fast"
}
+ },
+ "byok": {
+ "dataPrivacy": "Your data never passes through our servers",
+ "control": "Full control over cost and rate limits",
+ "flexibility": "Switch providers anytime without re-linking",
+ "cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"brand": {
@@ -1078,19 +1190,7 @@
"analytics": {
"enableTitle": "Anonyme Start-Analysen",
"enableDescription": "Anonyme grundlegende Nutzungsdaten teilen (optional)"
- },
- "search": "Suchen...",
- "appearance": "Erscheinungsbild",
- "account": "Konto",
- "about": "Über",
- "language": "Sprache",
- "theme": "Design",
- "fontSize": "Schriftgröße",
- "fontFamily": "Schriftart",
- "save": "Speichern",
- "cancel": "Abbrechen",
- "reset": "Zurücksetzen",
- "confirm": "Bestätigen"
+ }
},
"customNodes": {
"browserContent": "Browser-Inhalt",
@@ -1278,19 +1378,45 @@
"linkCopied": "Zusammenarbeitslink wurde kopiert.",
"copyManual": "Der Zugriff auf die Zwischenablage ist blockiert. Kopieren Sie den Link manuell aus dem Freigabedialog."
},
- "embed": "Einbetten",
- "embedCode": "Einbettungscode",
- "export": "Exportieren",
- "settings": "Freigabeeinstellungen",
- "anyoneWithLink": "Jeder mit Link",
- "viewOnly": "Nur ansehen",
- "canEdit": "Kann bearbeiten",
- "disable": "Freigabe deaktivieren",
- "enable": "Freigabe aktivieren",
- "invite": "Einladen",
- "pending": "Ausstehend",
- "revoke": "Widerrufen",
- "expired": "Abgelaufen"
+ "roomLink": "Zusammenarbeitslink",
+ "permissionsNote": "Jeder mit dem Link kann beitreten.",
+ "permissionsNoteSecondary": "Berechtigungskontrollen und dauerhafte Backend-Synchronisierung sind noch nicht konfiguriert.",
+ "viewerCount": {
+ "one": "1 Betrachter in dieser Sitzung.",
+ "many": "{{count}} Betrachter in dieser Sitzung."
+ },
+ "mode": {
+ "realtime": {
+ "title": "Echtzeit-Synchronisierung aktiv",
+ "body": "Peers, die diesen Link öffnen, können Live-Updates und Cursor über den aktuellen Peer-Transport sehen."
+ },
+ "waiting": {
+ "title": "Verbindung zur Echtzeit-Synchronisierung",
+ "body": "Diese Leinwand versucht noch, eine Live-Peer-Synchronisierung herzustellen. Falls dies nicht gelingt, bleibt die Sitzung lokal in diesem Browser."
+ },
+ "fallback": {
+ "title": "Nur-lokale Zusammenarbeit",
+ "body": "Ohne ein Backend-Relay oder unterstützten Peer-Transport bietet diese Sitzung keine dauerhafte Multi-User-Live-Synchronisierung außerhalb der aktuellen Browser-Laufzeit."
+ }
+ },
+ "cache": {
+ "syncing": {
+ "title": "Lokaler Raumcache wird synchronisiert",
+ "body": "Dieser Browser stellt noch den in IndexedDB zwischengespeicherten Raumzustand wieder her, bevor die Peer-Synchronisierung vollständig abgeschlossen ist."
+ },
+ "hydrated": {
+ "title": "Aus lokalem Cache wiederhergestellt",
+ "body": "Dieser Raum hatte bereits lokal zwischengespeicherten Zustand in diesem Browser, sodass die Leinwand wiederhergestellt werden konnte, bevor Peers sich wieder verbunden haben."
+ },
+ "ready": {
+ "title": "Lokaler Cache bereit",
+ "body": "Dieser Browser kann eine lokale IndexedDB-Kopie des Raums für Neuladen und Offline-Wiederherstellung auf diesem Gerät speichern."
+ },
+ "unavailable": {
+ "title": "Kein lokaler Raumcache",
+ "body": "Diese Zusammenarbeitssitzung verwendet derzeit keine browserseitige IndexedDB-Raumpersistenz."
+ }
+ }
},
"connectionPanel": {
"architecture": "Architektur",
@@ -1299,68 +1425,19 @@
"route": "Route",
"appearance": "Darstellung",
"condition": "Bedingung",
- "deleteConnection": "Verbindung löschen"
+ "deleteConnection": "Verbindung löschen",
+ "style": "Stil",
+ "actions": "Aktionen"
},
"sidebar": {
- "close": "Seitenleiste schließen",
- "searchPlaceholder": "Suchen...",
- "noResults": "Keine Ergebnisse",
- "nodes": "Knoten",
- "components": "Komponenten",
- "snippets": "Snippets",
- "addons": "Add-ons"
+ "close": "Seitenleiste schließen"
},
"contextMenu": {
"label": "Canvas-Kontextmenü"
},
- "canvas": {
- "addNodeShortcut": "Knoten hinzufügen",
- "aiChatPlaceholder": "Etwas erstellen...",
- "nodes": "Knoten",
- "connections": "Verbindungen",
- "elements": "Elemente",
- "selection": "Auswahl",
- "noSelection": "Keine Auswahl",
- "alignNodes": "Knoten ausrichten",
- "distributeNodes": "Knoten verteilen",
- "alignLeft": "Links ausrichten",
- "alignCenter": "Zentriert ausrichten",
- "alignRight": "Rechts ausrichten",
- "alignTop": "Oben ausrichten",
- "alignMiddle": "Mitte ausrichten",
- "alignBottom": "Unten ausrichten",
- "distributeHorizontally": "Horizontal verteilen",
- "distributeVertically": "Vertikal verteilen",
- "zoomToFit": "Einpassen zoom",
- "zoomToSelection": "Auf Auswahl zoomen",
- "locked": "Gesperrt",
- "unlock": "Entsperren",
- "lock": "Sperren",
- "bringToFront": "Nach vorne bringen",
- "sendToBack": "Nach hinten senden",
- "group": "Gruppieren",
- "ungroup": "Gruppierung aufheben"
- },
- "mindmap": {
- "addChild": "Unterthema hinzufügen",
- "addSibling": "Geschwisterthema hinzufügen",
- "addParent": "Übergeordnetes Thema hinzufügen",
- "delete": "Löschen",
- "insertAfter": "Danach einfügen",
- "insertBefore": "Davor einfügen"
- },
+ "canvas": {},
+ "mindmap": {},
"aiModel": {
- "title": "KI-Modell",
- "provider": "Anbieter",
- "model": "Modell",
- "temperature": "Temperatur",
- "maxTokens": "Maximale Token",
- "buttons": {
- "retry": "Erneut versuchen",
- "stop": "Stopp",
- "clear": "Löschen"
- },
- "thinking": "Denkt nach...",
- "error": "KI-Fehler"
+ "buttons": {}
}
}
diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json
index f8d8f511..3a8c5f91 100644
--- a/src/i18n/locales/en/translation.json
+++ b/src/i18n/locales/en/translation.json
@@ -156,7 +156,7 @@
"hintPlaybackGif": "Short loop for docs/social",
"hintRevealVideo": "Nodes fade in sequentially (WebM/MP4)",
"hintRevealGif": "Animated reveal for docs/social",
- "hintCinematicVideo": "Dark launch-style build",
+ "hintCinematicVideo": "Presentation-ready animated export",
"hintCinematicGif": "Dark social-ready loop",
"hintShareViewer": "Invite link for this room",
"hintShareEmbed": "Read-only viewer link",
@@ -284,28 +284,28 @@
"hint": "Names are local to this browser profile unless you export or sync them elsewhere.",
"closeDialog": "Close rename flow dialog"
},
- "continueTitle": "Continue with a recent action",
"homeEmptyTitle": "Create your first flow",
"homeEmptySubtitle": "Design enterprise-grade architectures instantly. Start from a blank canvas, describe your infrastructure with our AI builder, or use a tailored template.",
"homeBlankCanvas": "Blank Canvas",
"homeFlowpilotAI": "Flowpilot AI",
"homeTemplates": "Templates",
"homeImportFile": "Or import an existing file",
+ "deleteFlow": {
+ "title": "Delete flow",
+ "description": "This removes the local autosaved flow from this device.",
+ "confirmation": "Delete \"{{name}}\"?",
+ "hint": "This cannot be undone unless you have an exported backup or another copy.",
+ "closeDialog": "Close delete flow dialog"
+ },
+ "suggestionAI": "Flowpilot AI",
"suggestionBlank": "Blank canvas",
"suggestionBlankDesc": "Jump straight into the editor",
- "suggestionAI": "Flowpilot AI",
"suggestionAIDesc": "Start from a prompt",
"suggestionImport": "Import",
"suggestionImportDesc": "Bring in existing work",
"suggestionTemplates": "Templates",
"suggestionTemplatesDesc": "Start from a proven pattern",
- "deleteFlow": {
- "title": "Delete flow",
- "description": "This removes the local autosaved flow from this device.",
- "confirmation": "Delete \"{{name}}\"?",
- "hint": "This cannot be undone unless you have an exported backup or another copy.",
- "closeDialog": "Close delete flow dialog"
- }
+ "continueTitle": "Continue with a recent action"
},
"settings": {
"title": "Settings",
@@ -1302,7 +1302,7 @@
"diagrams": "Diagrams",
"assets": "Assets",
"node": "Shape",
- "stickyNote": "Sticky Note",
+ "stickyNote": "Note",
"section": "Section",
"text": "Text",
"other": "Other",
diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json
index df095cd6..6ed7325d 100644
--- a/src/i18n/locales/es/translation.json
+++ b/src/i18n/locales/es/translation.json
@@ -67,7 +67,10 @@
"distributeVertically": "Distribuir verticalmente",
"group": "Grupo",
"upload": "Subir",
- "rename": "Renombrar"
+ "rename": "Renombrar",
+ "export": "Exportar",
+ "exportDiagram": "Exportar diagrama",
+ "exportAs": "Exportar como"
},
"nav": {
"home": "Inicio",
@@ -123,13 +126,13 @@
"plantuml": "Exportar como PlantUML",
"openflowdsl": "Exportar como OpenFlow DSL",
"figma": "Exportar a Figma",
- "hintTransparent4K": "Transparent (4K)",
- "hintWhiteBg4K": "White Background (4K)",
+ "hintTransparent4K": "Transparente (4K)",
+ "hintWhiteBg4K": "Fondo blanco (4K)",
"jsonLabel": "JSON File",
- "hintDownload": "Download",
+ "hintDownload": "Descargar",
"openflowdslLabel": "{{appName}} DSL",
"hintClipboard": "Copiar",
- "figmaEditable": "Figma Editable",
+ "figmaEditable": "Figma editable",
"exportDiagram": "Export Diagram",
"exportAs": "Export As",
"actionCopy": "Copiar",
@@ -144,27 +147,28 @@
"shareEmbed": "Compartir e incrustar",
"hintShareViewer": "Enlace de invitación para esta sala",
"hintShareEmbed": "Enlace de visor de solo lectura",
- "readmeEmbed": "README Embed",
- "hintReadmeEmbed": "Copy Markdown snippet",
- "sectionImage": "Image",
+ "readmeEmbed": "Incrustar README",
+ "hintReadmeEmbed": "Copiar fragmento Markdown",
+ "sectionImage": "Imagen",
"sectionVideo": "Video",
- "sectionCode": "Code",
+ "sectionCode": "Código",
"shareSection": "Compartir lienzo",
- "revealVideo": "Reveal Video",
- "hintRevealVideo": "Nodes fade in sequentially (WebM/MP4)",
- "revealGif": "Reveal GIF",
- "hintRevealGif": "Animated reveal for docs/social",
- "cinematicVideo": "Cinematic Build Video",
+ "revealVideo": "Video de revelación",
+ "hintRevealVideo": "Los nodos aparecen secuencialmente (WebM/MP4)",
+ "revealGif": "GIF de revelación",
+ "hintRevealGif": "Revelación animada para docs/redes",
+ "cinematicVideo": "Video cinematográfico",
"hintCinematicVideo": "Dark launch-style build",
- "cinematicGif": "Cinematic Build GIF",
- "hintCinematicGif": "Dark social-ready loop",
- "actionDownload": "Download",
+ "cinematicGif": "GIF cinematográfico",
+ "hintCinematicGif": "Bucle oscuro para redes",
+ "actionDownload": "Descargar",
"hintDocument": "Documento",
"hintEditableSvg": "SVG editable",
"chooseFormat": "Elegir formato",
"speed": "Velocidad",
"resolution": "Resolución",
- "transparentBackground": "Fondo transparente"
+ "transparentBackground": "Fondo transparente",
+ "headingFormat": "Formato de exportación"
},
"import": {
"title": "Importar",
@@ -267,21 +271,21 @@
"hint": "Esto no se puede deshacer a menos que tengas una copia exportada u otra copia.",
"closeDialog": "Cerrar el diálogo de eliminar flujo"
},
+ "homeEmptyTitle": "Crea tu primer flujo",
+ "homeEmptySubtitle": "Diseña arquitecturas de nivel empresarial al instante. Comienza con un lienzo en blanco, describe tu infraestructura con nuestro constructor de IA, o usa una plantilla adaptada.",
+ "homeBlankCanvas": "Lienzo en blanco",
+ "homeFlowpilotAI": "Flowpilot IA",
+ "homeTemplates": "Plantillas",
+ "homeImportFile": "O importar un archivo existente",
+ "suggestionAI": "Flowpilot IA",
"suggestionBlank": "Lienzo en blanco",
"suggestionBlankDesc": "Saltar directamente al editor",
- "suggestionAI": "Flowpilot IA",
"suggestionAIDesc": "Empezar desde un mensaje",
"suggestionImport": "Importar",
"suggestionImportDesc": "Traer trabajo existente",
"suggestionTemplates": "Plantillas",
"suggestionTemplatesDesc": "Empezar desde un patrón probado",
- "continueTitle": "Continuar con una acción reciente",
- "homeEmptyTitle": "Crea tu primer flujo",
- "homeEmptySubtitle": "Diseña arquitecturas de nivel empresarial al instante. Comienza con un lienzo en blanco, describe tu infraestructura con nuestro constructor de IA, o usa una plantilla adaptada.",
- "homeBlankCanvas": "Lienzo en blanco",
- "homeFlowpilotAI": "Flowpilot IA",
- "homeTemplates": "Plantillas",
- "homeImportFile": "O importar un archivo existente"
+ "continueTitle": "Continuar con una acción reciente"
},
"settings": {
"title": "Configuración",
@@ -298,9 +302,9 @@
"about": "Acerca de",
"brand": "Configuración de marca",
"privacy": "Privacidad",
- "themeLight": "Light",
- "themeDark": "Dark",
- "themeSystem": "System"
+ "themeLight": "Claro",
+ "themeDark": "Oscuro",
+ "themeSystem": "Sistema"
},
"shortcuts": {
"title": "Atajos de teclado",
@@ -313,28 +317,28 @@
"help": "Ayuda"
},
"properties": {
- "title": "Properties",
- "shape": "Shape",
+ "title": "Propiedades",
+ "shape": "Forma",
"color": "Color",
- "icon": "Icon",
- "rotation": "Rotation",
- "transparency": "Transparency",
- "imageSettings": "Image Settings",
- "bulkShape": "Bulk Shape",
- "bulkColor": "Bulk Color",
- "bulkIcon": "Bulk Icon",
- "labelTransform": "Label Transform",
- "findReplace": "Find & Replace",
- "findLabel": "Find",
- "findPlaceholder": "Find text",
- "replaceLabel": "Replace",
- "replacePlaceholder": "Replace with",
- "prefixOptional": "Prefix (optional)",
- "suffixOptional": "Suffix (optional)",
- "useRegex": "Use Regex",
- "applyToSelectedNodes": "Apply to selected nodes",
- "selectFieldToApply": "Select a field to apply",
- "previewSummary": "Preview summary",
+ "icon": "Ícono",
+ "rotation": "Rotación",
+ "transparency": "Transparencia",
+ "imageSettings": "Configuración de imagen",
+ "bulkShape": "Forma (masiva)",
+ "bulkColor": "Color (masivo)",
+ "bulkIcon": "Ícono (masivo)",
+ "labelTransform": "Transformación de etiqueta",
+ "findReplace": "Buscar y reemplazar",
+ "findLabel": "Buscar",
+ "findPlaceholder": "Buscar texto",
+ "replaceLabel": "Reemplazar",
+ "replacePlaceholder": "Reemplazar con",
+ "prefixOptional": "Prefijo (opcional)",
+ "suffixOptional": "Sufijo (opcional)",
+ "useRegex": "Usar expresión regular",
+ "applyToSelectedNodes": "Aplicar a nodos seleccionados",
+ "selectFieldToApply": "Seleccionar campo para aplicar",
+ "previewSummary": "Resumen de vista previa",
"selectionSummary": "Resumen de selección"
},
"saveStatus": {
@@ -346,8 +350,14 @@
"model": "Modelo",
"apiKey": "Clave API",
"placeholder": "Describe tu diagrama...",
- "settingsSubtitle": "Configure your preferred AI provider, model, and API key below.",
- "generateWithFlowpilot": "Generar con Flowpilot"
+ "settingsSubtitle": "Configura tu proveedor de IA, modelo y clave API preferidos a continuación.",
+ "generateWithFlowpilot": "Generar con Flowpilot",
+ "howToGetKey": "Cómo obtener tu clave API",
+ "openConsole": "Abrir consola",
+ "pasteKeyStep": "Pégala en el campo de arriba; nunca se comparte con nosotros",
+ "customEndpointTitle": "¿Qué es un endpoint personalizado?",
+ "customModelHint": "Introduce el ID exacto del modelo para tu endpoint",
+ "privacyTitle": "Privacidad y cifrado"
},
"playback": {
"title": "Reproducción",
@@ -615,6 +625,13 @@
"edgeSyntax": "Usa flechas de arista de arquitectura `-->`, `<--` o `<-->` y calificadores de lado como `api:R --> L:db`.",
"nodeSyntax": "Usa declaraciones de nodo válidas: `service id(icon)[Label]`, `group id[Label]`, `junction id[Label]`.",
"fallback": "Desactiva el modo estricto de arquitectura para permitir la recuperación automática, o corrige los diagnósticos y vuelve a intentarlo."
+ },
+ "jumpToLine": "Ir a la línea {{line}}",
+ "diagnosticsGroup": {
+ "syntax": "Problemas de sintaxis",
+ "identity": "Problemas de identificador",
+ "recovery": "Advertencias de recuperación",
+ "general": "Diagnósticos"
}
},
"layout": {
@@ -757,9 +774,7 @@
"infra": "Infra",
"openapi": "OpenAPI",
"code": "Code",
- "codebase": "Repositorio",
- "mindmap": "Mapa mental",
- "markdown": "Markdown"
+ "codebase": "Repositorio"
},
"title": "Importar desde datos",
"description": "Importa código, infraestructura, SQL o especificaciones de API en tu diagrama.",
@@ -838,7 +853,9 @@
"edgeBundling": "Agrupar bordes hermanos",
"edgeBundlingDesc": "Mantener conexiones paralelas en carriles compartidos",
"architectureStrictMode": "Modo Estricto de Arquitectura",
- "architectureStrictModeDesc": "Bloquear importación de Mermaid cuando los diagnósticos de arquitectura incluyan problemas de recuperación/validación"
+ "architectureStrictModeDesc": "Bloquear importación de Mermaid cuando los diagnósticos de arquitectura incluyan problemas de recuperación/validación",
+ "miniMap": "Minimapa",
+ "miniMapDesc": "Mostrar minimapa en la esquina inferior derecha"
},
"ai": {
"provider": "Proveedor",
@@ -912,132 +929,227 @@
},
"models": {
"gemini": {
- "gemini-2": {
- "5-flash-lite": {
- "label": "Gemini 2.5 Flash Lite"
- },
- "5-flash": {
- "label": "Gemini 2.5 Flash"
- },
- "5-pro": {
- "label": "Gemini 2.5 Pro"
- }
- },
"gemini-3-flash": {
- "label": "Gemini 3 Flash"
+ "label": "Gemini 3 Flash",
+ "hint": "Frontier speed + intelligence",
+ "category": "Legacy",
+ "badge": "New"
},
"gemini-3-pro": {
- "label": "Gemini 3 Pro"
+ "label": "Gemini 3 Pro",
+ "hint": "Most powerful · Multimodal",
+ "category": "Legacy",
+ "badge": "New"
+ },
+ "gemini-2.5-flash-lite": {
+ "label": "2.5 Flash Lite",
+ "hint": "Fastest · Free tier default",
+ "category": "Speed",
+ "badge": "Default"
+ },
+ "gemini-2.5-flash": {
+ "label": "2.5 Flash",
+ "hint": "Best price/performance balance",
+ "category": "Speed"
+ },
+ "gemini-2.5-pro": {
+ "label": "2.5 Pro",
+ "hint": "Best reasoning · Complex diagrams",
+ "category": "Reasoning"
}
},
"openai": {
"gpt-5-mini": {
- "label": "GPT-5 Mini"
- },
- "gpt-5": {
- "2": {
- "label": "GPT-5.2"
- },
- "label": "GPT-5"
+ "label": "GPT-5 Mini",
+ "hint": "Fast · Cost-efficient",
+ "category": "Speed",
+ "badge": "Default"
},
"o4-mini": {
- "label": "O4-Mini"
+ "label": "O4-Mini",
+ "hint": "Advanced reasoning · Fast",
+ "category": "Reasoning",
+ "badge": "Reasoning"
},
"o3": {
- "label": "O3"
+ "label": "O3",
+ "hint": "Deep reasoning · Complex tasks",
+ "category": "Reasoning",
+ "badge": "Reasoning"
+ },
+ "gpt-5": {
+ "label": "GPT-5",
+ "hint": "Flagship model · Most capable",
+ "category": "Flagship"
+ },
+ "gpt-5.2": {
+ "label": "GPT-5.2",
+ "hint": "Latest update · Improved reasoning",
+ "category": "Reasoning",
+ "badge": "New"
}
},
"claude": {
"claude-haiku-4-5": {
- "label": "Claude Haiku 4.5"
+ "label": "Claude Haiku 4.5",
+ "hint": "Fastest · Most affordable",
+ "category": "Speed"
},
"claude-sonnet-4-5": {
- "label": "Claude Sonnet 4.5"
+ "label": "Claude Sonnet 4.5",
+ "hint": "Balanced intelligence & speed",
+ "category": "Flagship"
},
"claude-sonnet-4-6": {
- "label": "Claude Sonnet 4.6"
+ "label": "Claude Sonnet 4.6",
+ "hint": "Latest Sonnet · Best coding",
+ "category": "Flagship",
+ "badge": "Default"
},
"claude-opus-4-6": {
- "label": "Claude Opus 4.6"
+ "label": "Claude Opus 4.6",
+ "hint": "Most intelligent · 1M token context",
+ "category": "Reasoning",
+ "badge": "Flagship"
}
},
"groq": {
"meta-llama/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Free tier · Very fast",
+ "category": "Speed",
+ "badge": "Free"
},
"meta-llama/llama-4-maverick-17b-128e-instruct": {
- "label": "Llama 4 Maverick"
+ "label": "Llama 4 Maverick",
+ "hint": "More capable · Free tier",
+ "category": "Speed",
+ "badge": "Free"
},
"qwen/qwen3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
},
- "llama-3": {
- "3-70b-versatile": {
- "label": "Llama 3.3 70B"
- }
+ "llama-3.3-70b-versatile": {
+ "label": "Llama 3.3 70B Versatile",
+ "hint": "Versatile model",
+ "category": "Performance",
+ "badge": "Performance"
}
},
"nvidia": {
"meta/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Efficient · Multi-modal",
+ "category": "Speed"
},
"nvidia/nemotron-nano-12b-v2-vl": {
- "label": "Nemotron Nano 12B"
+ "label": "Nemotron Nano 12B",
+ "hint": "Lightweight · Vision-language · Fast",
+ "category": "Speed"
},
"deepseek/deepseek-v3-2": {
- "label": "DeepSeek V3-2"
+ "label": "DeepSeek V3-2",
+ "hint": "Latest · GPT-5 comparable",
+ "category": "Flagship",
+ "badge": "New"
},
"qwen/qwq-32b": {
- "label": "QwQ 32B"
+ "label": "QwQ 32B",
+ "hint": "Strong reasoning model",
+ "category": "Reasoning"
},
"moonshotai/kimi-k2-thinking": {
- "label": "Kimi K2 Thinking"
+ "label": "Kimi K2 Thinking",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"cerebras": {
"gpt-oss-120b": {
- "label": "GPT OSS 120B"
+ "label": "GPT OSS 120B",
+ "hint": "120B params · Fast on WSE-3",
+ "category": "Speed",
+ "badge": "Default"
},
"qwen-3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "2,403 tok/s · Industry fastest",
+ "category": "Speed",
+ "badge": "🚀 Fastest"
},
"qwen-3-235b-a22b": {
- "label": "Qwen 3 235B"
+ "label": "Qwen 3 235B",
+ "hint": "Flagship · Best quality",
+ "category": "Flagship",
+ "badge": "Flagship"
},
- "zai-glm-4": {
- "7": {
- "label": "Zai GLM 4.7"
- }
+ "zai-glm-4.7": {
+ "label": "Zai-GLM 4.7",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"mistral": {
"mistral-small-latest": {
- "label": "Mistral Small"
+ "label": "Mistral Small",
+ "hint": "Fast · Cost-efficient · 32k context",
+ "category": "Speed",
+ "badge": "Free"
},
"mistral-medium-latest": {
- "label": "Mistral Medium"
+ "label": "Mistral Medium",
+ "hint": "Balanced quality-cost · Best default",
+ "category": "Flagship",
+ "badge": "Default"
},
"mistral-large-latest": {
- "label": "Mistral Large"
+ "label": "Mistral Large",
+ "hint": "Most capable · 128k context · Flagship",
+ "category": "Flagship",
+ "badge": "Flagship"
},
"codestral-latest": {
- "label": "Codestral"
+ "label": "Codestral",
+ "hint": "Code-optimized · 256k context",
+ "category": "Coding",
+ "badge": "Code"
},
"pixtral-large-latest": {
- "label": "Pixtral Large"
+ "label": "Pixtral Large",
+ "hint": "Vision + reasoning · Multimodal",
+ "category": "Multimodal",
+ "badge": "Vision"
+ }
+ },
+ "custom": {
+ "custom": {
+ "label": "Custom Model",
+ "hint": "Enter your model ID below",
+ "category": "Custom"
}
}
},
"customEndpoints": {
"ollama": {
- "name": "Ollama"
+ "name": "Ollama",
+ "hint": "Local · Free"
},
"lmStudio": {
- "name": "LM Studio"
+ "name": "LM Studio",
+ "hint": "Local · Free"
},
"together": {
- "name": "Together AI"
+ "name": "Together AI",
+ "hint": "Cloud · Fast"
}
+ },
+ "byok": {
+ "dataPrivacy": "Your data never passes through our servers",
+ "control": "Full control over cost and rate limits",
+ "flexibility": "Switch providers anytime without re-linking",
+ "cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"brand": {
@@ -1078,19 +1190,7 @@
"analytics": {
"enableTitle": "Analíticas de inicio anónimas",
"enableDescription": "Compartir datos básicos de uso de forma anónima (opcional)"
- },
- "search": "Buscar...",
- "appearance": "Apariencia",
- "account": "Cuenta",
- "about": "Acerca de",
- "language": "Idioma",
- "theme": "Tema",
- "fontSize": "Tamaño de fuente",
- "fontFamily": "Familia de fuente",
- "save": "Guardar",
- "cancel": "Cancelar",
- "reset": "Restablecer",
- "confirm": "Confirmar"
+ }
},
"customNodes": {
"browserContent": "Contenido del navegador",
@@ -1278,19 +1378,45 @@
"linkCopied": "Se copió el enlace de colaboración.",
"copyManual": "El acceso al portapapeles está bloqueado. Copia el enlace manualmente desde el diálogo de compartir."
},
- "embed": "Insertar",
- "embedCode": "Código de inserción",
- "export": "Exportar",
- "settings": "Configuración de compartir",
- "anyoneWithLink": "Cualquiera con el enlace",
- "viewOnly": "Solo lectura",
- "canEdit": "Puede editar",
- "disable": "Desactivar compartir",
- "enable": "Activar compartir",
- "invite": "Invitar",
- "pending": "Pendiente",
- "revoke": "Revocar",
- "expired": "Expirado"
+ "roomLink": "Enlace de colaboración",
+ "permissionsNote": "Cualquiera con el enlace puede unirse.",
+ "permissionsNoteSecondary": "Los controles de permisos y la sincronización backend duradera aún no están configurados.",
+ "viewerCount": {
+ "one": "1 espectador en esta sesión.",
+ "many": "{{count}} espectadores en esta sesión."
+ },
+ "mode": {
+ "realtime": {
+ "title": "Sincronización en tiempo real activa",
+ "body": "Los pares que abran este enlace pueden ver actualizaciones en vivo y cursores a través del transporte par actual."
+ },
+ "waiting": {
+ "title": "Conectando a la sincronización en tiempo real",
+ "body": "Este lienzo aún está intentando establecer la sincronización par en vivo. Si no puede, la sesión se mantendrá solo local en este navegador."
+ },
+ "fallback": {
+ "title": "Colaboración solo local",
+ "body": "Sin un relay backend o transporte par compatible, esta sesión no proporciona sincronización en vivo multiusuario duradera fuera del runtime del navegador actual."
+ }
+ },
+ "cache": {
+ "syncing": {
+ "title": "Sincronizando caché local de la sala",
+ "body": "Este navegador aún está restaurando el estado de la sala cacheado en IndexedDB antes de que la sincronización par se establezca completamente."
+ },
+ "hydrated": {
+ "title": "Recuperado del caché local",
+ "body": "Esta sala ya tenía estado cacheado localmente en este navegador, por lo que el lienzo pudo restaurarse antes de que los pares se reconectaran."
+ },
+ "ready": {
+ "title": "Caché local listo",
+ "body": "Este navegador puede mantener una copia local IndexedDB de la sala para recarga y recuperación sin conexión en este dispositivo."
+ },
+ "unavailable": {
+ "title": "Sin caché local de sala",
+ "body": "Esta sesión de colaboración no está usando la persistencia IndexedDB de sala del navegador actualmente."
+ }
+ }
},
"connectionPanel": {
"architecture": "Arquitectura",
@@ -1299,68 +1425,19 @@
"route": "Ruta",
"appearance": "Apariencia",
"condition": "Condición",
- "deleteConnection": "Eliminar conexión"
+ "deleteConnection": "Eliminar conexión",
+ "style": "Estilo",
+ "actions": "Acciones"
},
"sidebar": {
- "close": "Cerrar barra lateral",
- "searchPlaceholder": "Buscar...",
- "noResults": "Sin resultados",
- "nodes": "Nodos",
- "components": "Componentes",
- "snippets": "Fragmentos",
- "addons": "Complementos"
+ "close": "Cerrar barra lateral"
},
"contextMenu": {
"label": "Menú contextual del lienzo"
},
- "canvas": {
- "addNodeShortcut": "Agregar nodo",
- "aiChatPlaceholder": "Crear algo...",
- "nodes": "Nodos",
- "connections": "Conexiones",
- "elements": "Elementos",
- "selection": "Selección",
- "noSelection": "Sin selección",
- "alignNodes": "Alinear nodos",
- "distributeNodes": "Distribuir nodos",
- "alignLeft": "Alinear a la izquierda",
- "alignCenter": "Alinear al centro",
- "alignRight": "Alinear a la derecha",
- "alignTop": "Alinear arriba",
- "alignMiddle": "Alinear al medio",
- "alignBottom": "Alinear abajo",
- "distributeHorizontally": "Distribuir horizontalmente",
- "distributeVertically": "Distribuir verticalmente",
- "zoomToFit": "Ajustar zoom",
- "zoomToSelection": "Zoom a selección",
- "locked": "Bloqueado",
- "unlock": "Desbloquear",
- "lock": "Bloquear",
- "bringToFront": "Traer al frente",
- "sendToBack": "Enviar atrás",
- "group": "Agrupar",
- "ungroup": "Desagrupar"
- },
- "mindmap": {
- "addChild": "Agregar subtema",
- "addSibling": "Agregar tema hermano",
- "addParent": "Agregar tema padre",
- "delete": "Eliminar",
- "insertAfter": "Insertar después",
- "insertBefore": "Insertar antes"
- },
+ "canvas": {},
+ "mindmap": {},
"aiModel": {
- "title": "Modelo de IA",
- "provider": "Proveedor",
- "model": "Modelo",
- "temperature": "Temperatura",
- "maxTokens": "Máximo de tokens",
- "buttons": {
- "retry": "Reintentar",
- "stop": "Detener",
- "clear": "Limpiar"
- },
- "thinking": "Pensando...",
- "error": "Error de IA"
+ "buttons": {}
}
}
diff --git a/src/i18n/locales/fr/translation.json b/src/i18n/locales/fr/translation.json
index 8f10646d..4720325f 100644
--- a/src/i18n/locales/fr/translation.json
+++ b/src/i18n/locales/fr/translation.json
@@ -67,7 +67,10 @@
"distributeVertically": "Distribuer verticalement",
"group": "Groupe",
"upload": "Téléverser",
- "rename": "Renommer"
+ "rename": "Renommer",
+ "export": "Exporter",
+ "exportDiagram": "Exporter le diagramme",
+ "exportAs": "Exporter en tant que"
},
"nav": {
"home": "Accueil",
@@ -124,12 +127,12 @@
"openflowdsl": "Exporter en OpenFlow DSL",
"figma": "Exporter vers Figma",
"hintTransparent4K": "Transparent (4K)",
- "hintWhiteBg4K": "White Background (4K)",
+ "hintWhiteBg4K": "Fond blanc (4K)",
"jsonLabel": "JSON File",
- "hintDownload": "Download",
+ "hintDownload": "Télécharger",
"openflowdslLabel": "{{appName}} DSL",
"hintClipboard": "Copier",
- "figmaEditable": "Figma Editable",
+ "figmaEditable": "Figma éditable",
"exportDiagram": "Export Diagram",
"exportAs": "Export As",
"actionCopy": "Copier",
@@ -144,27 +147,28 @@
"shareEmbed": "Partager et intégrer",
"hintShareViewer": "Lien d’invitation pour cette salle",
"hintShareEmbed": "Lien de visualisation en lecture seule",
- "readmeEmbed": "README Embed",
- "hintReadmeEmbed": "Copy Markdown snippet",
+ "readmeEmbed": "Intégration README",
+ "hintReadmeEmbed": "Copier un extrait Markdown",
"sectionImage": "Image",
- "sectionVideo": "Video",
+ "sectionVideo": "Vidéo",
"sectionCode": "Code",
"shareSection": "Partager le canevas",
- "revealVideo": "Reveal Video",
- "hintRevealVideo": "Nodes fade in sequentially (WebM/MP4)",
- "revealGif": "Reveal GIF",
- "hintRevealGif": "Animated reveal for docs/social",
- "cinematicVideo": "Cinematic Build Video",
+ "revealVideo": "Vidéo de révélation",
+ "hintRevealVideo": "Les nœuds apparaissent séquentiellement (WebM/MP4)",
+ "revealGif": "GIF de révélation",
+ "hintRevealGif": "Révélation animée pour docs/réseaux",
+ "cinematicVideo": "Vidéo cinématique",
"hintCinematicVideo": "Dark launch-style build",
- "cinematicGif": "Cinematic Build GIF",
- "hintCinematicGif": "Dark social-ready loop",
- "actionDownload": "Download",
+ "cinematicGif": "GIF cinématique",
+ "hintCinematicGif": "Boucle sombre pour réseaux",
+ "actionDownload": "Télécharger",
"hintDocument": "Document",
"hintEditableSvg": "SVG modifiable",
"chooseFormat": "Choisir un format",
"speed": "Vitesse",
"resolution": "Résolution",
- "transparentBackground": "Arrière-plan transparent"
+ "transparentBackground": "Arrière-plan transparent",
+ "headingFormat": "Format d'exportation"
},
"import": {
"title": "Importer",
@@ -267,21 +271,21 @@
"hint": "Cette action est irréversible sauf si vous avez un export de sauvegarde ou une autre copie.",
"closeDialog": "Fermer la boîte de dialogue de suppression du flux"
},
- "suggestionBlank": "Canevas vierge",
- "suggestionBlankDesc": "aller directement dans l'éditeur",
- "suggestionAI": "Flowpilot IA",
- "suggestionAIDesc": "Commencer à partir d'une invite",
- "suggestionImport": "Importer",
- "suggestionImportDesc": "Importer un travail existant",
- "suggestionTemplates": "Modèles",
- "suggestionTemplatesDesc": "Commencer à partir d'un modèle éprouvé",
- "continueTitle": "Continuer avec une action récente",
"homeEmptyTitle": "Créez votre premier flux",
"homeEmptySubtitle": "Concevez des architectures de qualité entreprise instantanément. Commencez avec un canevas vierge, décrivez votre infrastructure avec notre constructeur IA, ou utilisez un modèle adapté.",
"homeBlankCanvas": "Canevas vierge",
"homeFlowpilotAI": "Flowpilot IA",
"homeTemplates": "Modèles",
- "homeImportFile": "Ou importer un fichier existant"
+ "homeImportFile": "Ou importer un fichier existant",
+ "suggestionAI": "Flowpilot IA",
+ "suggestionBlank": "Canevas vierge",
+ "suggestionBlankDesc": "Accéder directement à l'éditeur",
+ "suggestionAIDesc": "Commencer avec une invite",
+ "suggestionImport": "Importer",
+ "suggestionImportDesc": "Importer un travail existant",
+ "suggestionTemplates": "Modèles",
+ "suggestionTemplatesDesc": "Commencer avec un modèle éprouvé",
+ "continueTitle": "Continuer avec une action récente"
},
"settings": {
"title": "Paramètres",
@@ -298,9 +302,9 @@
"about": "À propos",
"brand": "Paramètres de marque",
"privacy": "Confidentialité",
- "themeLight": "Light",
- "themeDark": "Dark",
- "themeSystem": "System"
+ "themeLight": "Clair",
+ "themeDark": "Sombre",
+ "themeSystem": "Système"
},
"shortcuts": {
"title": "Raccourcis clavier",
@@ -313,28 +317,28 @@
"help": "Aide"
},
"properties": {
- "title": "Properties",
- "shape": "Shape",
- "color": "Color",
- "icon": "Icon",
+ "title": "Propriétés",
+ "shape": "Forme",
+ "color": "Couleur",
+ "icon": "Icône",
"rotation": "Rotation",
- "transparency": "Transparency",
- "imageSettings": "Image Settings",
- "bulkShape": "Bulk Shape",
- "bulkColor": "Bulk Color",
- "bulkIcon": "Bulk Icon",
- "labelTransform": "Label Transform",
- "findReplace": "Find & Replace",
- "findLabel": "Find",
- "findPlaceholder": "Find text",
- "replaceLabel": "Replace",
- "replacePlaceholder": "Replace with",
- "prefixOptional": "Prefix (optional)",
- "suffixOptional": "Suffix (optional)",
- "useRegex": "Use Regex",
- "applyToSelectedNodes": "Apply to selected nodes",
- "selectFieldToApply": "Select a field to apply",
- "previewSummary": "Preview summary",
+ "transparency": "Transparence",
+ "imageSettings": "Paramètres d'image",
+ "bulkShape": "Forme (groupée)",
+ "bulkColor": "Couleur (groupée)",
+ "bulkIcon": "Icône (groupée)",
+ "labelTransform": "Transformation de l'étiquette",
+ "findReplace": "Rechercher & Remplacer",
+ "findLabel": "Rechercher",
+ "findPlaceholder": "Rechercher du texte",
+ "replaceLabel": "Remplacer",
+ "replacePlaceholder": "Remplacer par",
+ "prefixOptional": "Préfixe (optionnel)",
+ "suffixOptional": "Suffixe (optionnel)",
+ "useRegex": "Utiliser une expression régulière",
+ "applyToSelectedNodes": "Appliquer aux nœuds sélectionnés",
+ "selectFieldToApply": "Sélectionner un champ à appliquer",
+ "previewSummary": "Résumé de l'aperçu",
"selectionSummary": "Résumé de la sélection"
},
"saveStatus": {
@@ -346,8 +350,14 @@
"model": "Modèle",
"apiKey": "Clé API",
"placeholder": "Décrivez votre diagramme...",
- "settingsSubtitle": "Configure your preferred AI provider, model, and API key below.",
- "generateWithFlowpilot": "Générer avec Flowpilot"
+ "settingsSubtitle": "Configurez votre fournisseur IA, modèle et clé API préférés ci-dessous.",
+ "generateWithFlowpilot": "Générer avec Flowpilot",
+ "howToGetKey": "Comment obtenir votre clé API",
+ "openConsole": "Ouvrir la console",
+ "pasteKeyStep": "Collez-la dans le champ ci-dessus — elle n'est jamais partagée avec nous",
+ "customEndpointTitle": "Qu'est-ce qu'un point de terminaison personnalisé ?",
+ "customModelHint": "Entrez l'ID exact du modèle pour votre point de terminaison",
+ "privacyTitle": "Confidentialité et chiffrement"
},
"playback": {
"title": "Lecture",
@@ -615,6 +625,13 @@
"edgeSyntax": "Utilisez les flèches d’arête d’architecture `-->`, `<--` ou `<-->` et des qualificateurs de côté comme `api:R --> L:db`.",
"nodeSyntax": "Utilisez des déclarations de nœud valides : `service id(icon)[Label]`, `group id[Label]`, `junction id[Label]`.",
"fallback": "Désactivez le mode strict d’architecture pour autoriser la récupération automatique, ou corrigez les diagnostics puis réessayez."
+ },
+ "jumpToLine": "Aller à la ligne {{line}}",
+ "diagnosticsGroup": {
+ "syntax": "Problèmes de syntaxe",
+ "identity": "Problèmes d'identifiant",
+ "recovery": "Avertissements de récupération",
+ "general": "Diagnostics"
}
},
"layout": {
@@ -757,9 +774,7 @@
"infra": "Infra",
"openapi": "OpenAPI",
"code": "Code",
- "codebase": "Dépôt",
- "mindmap": "Carte mentale",
- "markdown": "Markdown"
+ "codebase": "Dépôt"
},
"title": "Importer depuis des données",
"description": "Importez du code, de l'infrastructure, du SQL ou des spécifications d'API dans votre diagramme.",
@@ -838,7 +853,9 @@
"edgeBundling": "Regrouper les bords siblings",
"edgeBundlingDesc": "Garder les connexions parallèles sur des voies partagées",
"architectureStrictMode": "Mode Strict Architecture",
- "architectureStrictModeDesc": "Bloquer l'import Mermaid lorsque les diagnostics d'architecture incluent des problèmes de récupération/validation"
+ "architectureStrictModeDesc": "Bloquer l'import Mermaid lorsque les diagnostics d'architecture incluent des problèmes de récupération/validation",
+ "miniMap": "Mini carte",
+ "miniMapDesc": "Afficher la mini carte en bas à droite"
},
"ai": {
"provider": "Fournisseur",
@@ -912,132 +929,227 @@
},
"models": {
"gemini": {
- "gemini-2": {
- "5-flash-lite": {
- "label": "Gemini 2.5 Flash Lite"
- },
- "5-flash": {
- "label": "Gemini 2.5 Flash"
- },
- "5-pro": {
- "label": "Gemini 2.5 Pro"
- }
- },
"gemini-3-flash": {
- "label": "Gemini 3 Flash"
+ "label": "Gemini 3 Flash",
+ "hint": "Frontier speed + intelligence",
+ "category": "Legacy",
+ "badge": "New"
},
"gemini-3-pro": {
- "label": "Gemini 3 Pro"
+ "label": "Gemini 3 Pro",
+ "hint": "Most powerful · Multimodal",
+ "category": "Legacy",
+ "badge": "New"
+ },
+ "gemini-2.5-flash-lite": {
+ "label": "2.5 Flash Lite",
+ "hint": "Fastest · Free tier default",
+ "category": "Speed",
+ "badge": "Default"
+ },
+ "gemini-2.5-flash": {
+ "label": "2.5 Flash",
+ "hint": "Best price/performance balance",
+ "category": "Speed"
+ },
+ "gemini-2.5-pro": {
+ "label": "2.5 Pro",
+ "hint": "Best reasoning · Complex diagrams",
+ "category": "Reasoning"
}
},
"openai": {
"gpt-5-mini": {
- "label": "GPT-5 Mini"
- },
- "gpt-5": {
- "2": {
- "label": "GPT-5.2"
- },
- "label": "GPT-5"
+ "label": "GPT-5 Mini",
+ "hint": "Fast · Cost-efficient",
+ "category": "Speed",
+ "badge": "Default"
},
"o4-mini": {
- "label": "O4-Mini"
+ "label": "O4-Mini",
+ "hint": "Advanced reasoning · Fast",
+ "category": "Reasoning",
+ "badge": "Reasoning"
},
"o3": {
- "label": "O3"
+ "label": "O3",
+ "hint": "Deep reasoning · Complex tasks",
+ "category": "Reasoning",
+ "badge": "Reasoning"
+ },
+ "gpt-5": {
+ "label": "GPT-5",
+ "hint": "Flagship model · Most capable",
+ "category": "Flagship"
+ },
+ "gpt-5.2": {
+ "label": "GPT-5.2",
+ "hint": "Latest update · Improved reasoning",
+ "category": "Reasoning",
+ "badge": "New"
}
},
"claude": {
"claude-haiku-4-5": {
- "label": "Claude Haiku 4.5"
+ "label": "Claude Haiku 4.5",
+ "hint": "Fastest · Most affordable",
+ "category": "Speed"
},
"claude-sonnet-4-5": {
- "label": "Claude Sonnet 4.5"
+ "label": "Claude Sonnet 4.5",
+ "hint": "Balanced intelligence & speed",
+ "category": "Flagship"
},
"claude-sonnet-4-6": {
- "label": "Claude Sonnet 4.6"
+ "label": "Claude Sonnet 4.6",
+ "hint": "Latest Sonnet · Best coding",
+ "category": "Flagship",
+ "badge": "Default"
},
"claude-opus-4-6": {
- "label": "Claude Opus 4.6"
+ "label": "Claude Opus 4.6",
+ "hint": "Most intelligent · 1M token context",
+ "category": "Reasoning",
+ "badge": "Flagship"
}
},
"groq": {
"meta-llama/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Free tier · Very fast",
+ "category": "Speed",
+ "badge": "Free"
},
"meta-llama/llama-4-maverick-17b-128e-instruct": {
- "label": "Llama 4 Maverick"
+ "label": "Llama 4 Maverick",
+ "hint": "More capable · Free tier",
+ "category": "Speed",
+ "badge": "Free"
},
"qwen/qwen3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
},
- "llama-3": {
- "3-70b-versatile": {
- "label": "Llama 3.3 70B"
- }
+ "llama-3.3-70b-versatile": {
+ "label": "Llama 3.3 70B Versatile",
+ "hint": "Versatile model",
+ "category": "Performance",
+ "badge": "Performance"
}
},
"nvidia": {
"meta/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Efficient · Multi-modal",
+ "category": "Speed"
},
"nvidia/nemotron-nano-12b-v2-vl": {
- "label": "Nemotron Nano 12B"
+ "label": "Nemotron Nano 12B",
+ "hint": "Lightweight · Vision-language · Fast",
+ "category": "Speed"
},
"deepseek/deepseek-v3-2": {
- "label": "DeepSeek V3-2"
+ "label": "DeepSeek V3-2",
+ "hint": "Latest · GPT-5 comparable",
+ "category": "Flagship",
+ "badge": "New"
},
"qwen/qwq-32b": {
- "label": "QwQ 32B"
+ "label": "QwQ 32B",
+ "hint": "Strong reasoning model",
+ "category": "Reasoning"
},
"moonshotai/kimi-k2-thinking": {
- "label": "Kimi K2 Thinking"
+ "label": "Kimi K2 Thinking",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"cerebras": {
"gpt-oss-120b": {
- "label": "GPT OSS 120B"
+ "label": "GPT OSS 120B",
+ "hint": "120B params · Fast on WSE-3",
+ "category": "Speed",
+ "badge": "Default"
},
"qwen-3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "2,403 tok/s · Industry fastest",
+ "category": "Speed",
+ "badge": "🚀 Fastest"
},
"qwen-3-235b-a22b": {
- "label": "Qwen 3 235B"
+ "label": "Qwen 3 235B",
+ "hint": "Flagship · Best quality",
+ "category": "Flagship",
+ "badge": "Flagship"
},
- "zai-glm-4": {
- "7": {
- "label": "Zai GLM 4.7"
- }
+ "zai-glm-4.7": {
+ "label": "Zai-GLM 4.7",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"mistral": {
"mistral-small-latest": {
- "label": "Mistral Small"
+ "label": "Mistral Small",
+ "hint": "Fast · Cost-efficient · 32k context",
+ "category": "Speed",
+ "badge": "Free"
},
"mistral-medium-latest": {
- "label": "Mistral Medium"
+ "label": "Mistral Medium",
+ "hint": "Balanced quality-cost · Best default",
+ "category": "Flagship",
+ "badge": "Default"
},
"mistral-large-latest": {
- "label": "Mistral Large"
+ "label": "Mistral Large",
+ "hint": "Most capable · 128k context · Flagship",
+ "category": "Flagship",
+ "badge": "Flagship"
},
"codestral-latest": {
- "label": "Codestral"
+ "label": "Codestral",
+ "hint": "Code-optimized · 256k context",
+ "category": "Coding",
+ "badge": "Code"
},
"pixtral-large-latest": {
- "label": "Pixtral Large"
+ "label": "Pixtral Large",
+ "hint": "Vision + reasoning · Multimodal",
+ "category": "Multimodal",
+ "badge": "Vision"
+ }
+ },
+ "custom": {
+ "custom": {
+ "label": "Custom Model",
+ "hint": "Enter your model ID below",
+ "category": "Custom"
}
}
},
"customEndpoints": {
"ollama": {
- "name": "Ollama"
+ "name": "Ollama",
+ "hint": "Local · Free"
},
"lmStudio": {
- "name": "LM Studio"
+ "name": "LM Studio",
+ "hint": "Local · Free"
},
"together": {
- "name": "Together AI"
+ "name": "Together AI",
+ "hint": "Cloud · Fast"
}
+ },
+ "byok": {
+ "dataPrivacy": "Your data never passes through our servers",
+ "control": "Full control over cost and rate limits",
+ "flexibility": "Switch providers anytime without re-linking",
+ "cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"brand": {
@@ -1078,19 +1190,7 @@
"analytics": {
"enableTitle": "Analyses de lancement anonymes",
"enableDescription": "Partager des données d’usage de base de manière anonyme (optionnel)"
- },
- "search": "Rechercher...",
- "appearance": "Apparence",
- "account": "Compte",
- "about": "À propos",
- "language": "Langue",
- "theme": "Thème",
- "fontSize": "Taille de police",
- "fontFamily": "Police",
- "save": "Enregistrer",
- "cancel": "Annuler",
- "reset": "Réinitialiser",
- "confirm": "Confirmer"
+ }
},
"customNodes": {
"browserContent": "Contenu du navigateur",
@@ -1278,19 +1378,45 @@
"linkCopied": "Le lien de collaboration a été copié.",
"copyManual": "L’accès au presse-papiers est bloqué. Copiez le lien manuellement depuis la boîte de dialogue de partage."
},
- "embed": "Intégrer",
- "embedCode": "Code d'intégration",
- "export": "Exporter",
- "settings": "Paramètres de partage",
- "anyoneWithLink": "Toute personne avec le lien",
- "viewOnly": "Lecture seule",
- "canEdit": "Peut modifier",
- "disable": "Désactiver le partage",
- "enable": "Activer le partage",
- "invite": "Inviter",
- "pending": "En attente",
- "revoke": "Révoquer",
- "expired": "Expiré"
+ "roomLink": "Lien de collaboration",
+ "permissionsNote": "Toute personne disposant du lien peut rejoindre.",
+ "permissionsNoteSecondary": "Les contrôles d'autorisation et la synchronisation backend durable ne sont pas encore configurés.",
+ "viewerCount": {
+ "one": "1 spectateur dans cette session.",
+ "many": "{{count}} spectateurs dans cette session."
+ },
+ "mode": {
+ "realtime": {
+ "title": "Synchronisation en temps réel active",
+ "body": "Les pairs qui ouvrent ce lien peuvent voir les mises à jour en direct et les curseurs via le transport pair actuel."
+ },
+ "waiting": {
+ "title": "Connexion à la synchronisation en temps réel",
+ "body": "Ce caneaux essaie toujours d'établir une synchronisation pair-à-pair en direct. En cas d'échec, la session restera locale dans ce navigateur."
+ },
+ "fallback": {
+ "title": "Collaboration locale uniquement",
+ "body": "Sans relais backend ni transport pair supporté, cette session ne fournit pas de synchronisation en direct multi-utilisateurs durable en dehors de l'exécution du navigateur actuel."
+ }
+ },
+ "cache": {
+ "syncing": {
+ "title": "Synchronisation du cache local de la salle",
+ "body": "Ce navigateur restaure encore l'état de la salle mis en cache dans IndexedDB avant que la synchronisation pair ne soit pleinement établie."
+ },
+ "hydrated": {
+ "title": "Récupéré depuis le cache local",
+ "body": "Cette salle avait déjà un état mis en cache localement dans ce navigateur, donc le canevas a pu être restauré avant que les pairs ne se reconnectent."
+ },
+ "ready": {
+ "title": "Cache local prêt",
+ "body": "Ce navigateur peut conserver une copie locale IndexedDB de la salle pour le rechargement et la récupération hors ligne sur cet appareil."
+ },
+ "unavailable": {
+ "title": "Pas de cache local de salle",
+ "body": "Cette session de collaboration n'utilise pas la persistance IndexedDB de la salle dans le navigateur actuellement."
+ }
+ }
},
"connectionPanel": {
"architecture": "Architecture",
@@ -1299,68 +1425,19 @@
"route": "Trajet",
"appearance": "Apparence",
"condition": "Condition",
- "deleteConnection": "Supprimer la connexion"
+ "deleteConnection": "Supprimer la connexion",
+ "style": "Style",
+ "actions": "Actions"
},
"sidebar": {
- "close": "Fermer la barre latérale",
- "searchPlaceholder": "Rechercher...",
- "noResults": "Aucun résultat",
- "nodes": "Nœuds",
- "components": "Composants",
- "snippets": "Extraits",
- "addons": "Add-ons"
+ "close": "Fermer la barre latérale"
},
"contextMenu": {
"label": "Menu contextuel du canevas"
},
- "canvas": {
- "addNodeShortcut": "Ajouter un nœud",
- "aiChatPlaceholder": "Créer quelque chose...",
- "nodes": "Nœuds",
- "connections": "Connexions",
- "elements": "Éléments",
- "selection": "Sélection",
- "noSelection": "Aucune sélection",
- "alignNodes": "Aligner les nœuds",
- "distributeNodes": "Distribuer les nœuds",
- "alignLeft": "Aligner à gauche",
- "alignCenter": "Aligner au centre",
- "alignRight": "Aligner à droite",
- "alignTop": "Aligner en haut",
- "alignMiddle": "Aligner au milieu",
- "alignBottom": "Aligner en bas",
- "distributeHorizontally": "Distribuer horizontalement",
- "distributeVertically": "Distribuer verticalement",
- "zoomToFit": "Zoom pour ajuster",
- "zoomToSelection": "Zoom sur la sélection",
- "locked": "Verrouillé",
- "unlock": "Déverrouiller",
- "lock": "Verrouiller",
- "bringToFront": "Mettre au premier plan",
- "sendToBack": "Envoyer à l'arrière",
- "group": "Grouper",
- "ungroup": "Dissocier"
- },
- "mindmap": {
- "addChild": "Ajouter un sous-sujet",
- "addSibling": "Ajouter un sujet frère",
- "addParent": "Ajouter un sujet parent",
- "delete": "Supprimer",
- "insertAfter": "Insérer après",
- "insertBefore": "Insérer avant"
- },
+ "canvas": {},
+ "mindmap": {},
"aiModel": {
- "title": "Modèle IA",
- "provider": "Fournisseur",
- "model": "Modèle",
- "temperature": "Température",
- "maxTokens": "Jetons max",
- "buttons": {
- "retry": "Réessayer",
- "stop": "Arrêter",
- "clear": "Effacer"
- },
- "thinking": "Réflexion...",
- "error": "Erreur IA"
+ "buttons": {}
}
}
diff --git a/src/i18n/locales/ja/translation.json b/src/i18n/locales/ja/translation.json
index c7986127..bc01444f 100644
--- a/src/i18n/locales/ja/translation.json
+++ b/src/i18n/locales/ja/translation.json
@@ -67,7 +67,10 @@
"distributeVertically": "垂直方向に分布",
"group": "グループ",
"upload": "アップロード",
- "rename": "名前を変更"
+ "rename": "名前を変更",
+ "export": "エクスポート",
+ "exportDiagram": "図をエクスポート",
+ "exportAs": "名前を付けてエクスポート"
},
"nav": {
"home": "ホーム",
@@ -123,13 +126,13 @@
"plantuml": "PlantUMLとしてエクスポート",
"openflowdsl": "OpenFlow DSLとしてエクスポート",
"figma": "Figmaにエクスポート",
- "hintTransparent4K": "Transparent (4K)",
- "hintWhiteBg4K": "White Background (4K)",
+ "hintTransparent4K": "透明 (4K)",
+ "hintWhiteBg4K": "白背景 (4K)",
"jsonLabel": "JSON File",
- "hintDownload": "Download",
+ "hintDownload": "ダウンロード",
"openflowdslLabel": "{{appName}} DSL",
"hintClipboard": "コピー",
- "figmaEditable": "Figma Editable",
+ "figmaEditable": "Figma 編集可能",
"exportDiagram": "Export Diagram",
"exportAs": "Export As",
"actionCopy": "コピー",
@@ -144,27 +147,28 @@
"shareEmbed": "共有と埋め込み",
"hintShareViewer": "このルームへの招待リンク",
"hintShareEmbed": "読み取り専用ビューアーリンク",
- "readmeEmbed": "README Embed",
- "hintReadmeEmbed": "Copy Markdown snippet",
- "sectionImage": "Image",
- "sectionVideo": "Video",
- "sectionCode": "Code",
+ "readmeEmbed": "README 埋め込み",
+ "hintReadmeEmbed": "Markdown スニペットをコピー",
+ "sectionImage": "画像",
+ "sectionVideo": "動画",
+ "sectionCode": "コード",
"shareSection": "キャンバスを共有",
- "revealVideo": "Reveal Video",
- "hintRevealVideo": "Nodes fade in sequentially (WebM/MP4)",
- "revealGif": "Reveal GIF",
- "hintRevealGif": "Animated reveal for docs/social",
- "cinematicVideo": "Cinematic Build Video",
+ "revealVideo": "リビール動画",
+ "hintRevealVideo": "ノードが順番にフェードイン (WebM/MP4)",
+ "revealGif": "リビール GIF",
+ "hintRevealGif": "ドキュメント/SNS向けアニメーション表示",
+ "cinematicVideo": "シネマティブuild動画",
"hintCinematicVideo": "Dark launch-style build",
- "cinematicGif": "Cinematic Build GIF",
- "hintCinematicGif": "Dark social-ready loop",
- "actionDownload": "Download",
+ "cinematicGif": "シネマティブuild GIF",
+ "hintCinematicGif": "ダークソーシャルループ",
+ "actionDownload": "ダウンロード",
"hintDocument": "ドキュメント",
"hintEditableSvg": "編集可能な SVG",
"chooseFormat": "形式を選択",
"speed": "速度",
"resolution": "解像度",
- "transparentBackground": "透明な背景"
+ "transparentBackground": "透明な背景",
+ "headingFormat": "エクスポート形式"
},
"import": {
"title": "インポート",
@@ -267,21 +271,21 @@
"hint": "エクスポートしたバックアップまたは別のコピーがない限り、この操作は元に戻せません。",
"closeDialog": "フロー削除ダイアログを閉じる"
},
+ "homeEmptyTitle": "最初のフローを作成",
+ "homeEmptySubtitle": "エンタープライズグレードのアーキテクチャを即座に設計。空白のキャンバスから始めるか、AIビルダーでインフラを描述するか、カスタマイズテンプレートを使用してください。",
+ "homeBlankCanvas": "空白のキャンバス",
+ "homeFlowpilotAI": "フローパイロット人工知能",
+ "homeTemplates": "テンプレート",
+ "homeImportFile": "または既存のファイルをインポート",
+ "suggestionAI": "フローパイロット人工知能",
"suggestionBlank": "空白のキャンバス",
"suggestionBlankDesc": "エディタに直接ジャンプ",
- "suggestionAI": "フローパイロット人工知能",
"suggestionAIDesc": "プロンプトから始める",
"suggestionImport": "インポート",
"suggestionImportDesc": "既存の作品を持ち込む",
"suggestionTemplates": "テンプレート",
"suggestionTemplatesDesc": "実証済みのパターンから始める",
- "continueTitle": "最近の操作を続ける",
- "homeEmptyTitle": "最初のフローを作成",
- "homeEmptySubtitle": "エンタープライズグレードのアーキテクチャを即座に設計。空白のキャンバスから始めるか、AIビルダーでインフラを描述するか、カスタマイズテンプレートを使用してください。",
- "homeBlankCanvas": "空白のキャンバス",
- "homeFlowpilotAI": "フローパイロット人工知能",
- "homeTemplates": "テンプレート",
- "homeImportFile": "または既存のファイルをインポート"
+ "continueTitle": "最近の操作を続ける"
},
"settings": {
"title": "設定",
@@ -313,28 +317,28 @@
"help": "ヘルプ"
},
"properties": {
- "title": "Properties",
- "shape": "Shape",
- "color": "Color",
- "icon": "Icon",
- "rotation": "Rotation",
- "transparency": "Transparency",
- "imageSettings": "Image Settings",
- "bulkShape": "Bulk Shape",
- "bulkColor": "Bulk Color",
- "bulkIcon": "Bulk Icon",
- "labelTransform": "Label Transform",
- "findReplace": "Find & Replace",
- "findLabel": "Find",
- "findPlaceholder": "Find text",
- "replaceLabel": "Replace",
- "replacePlaceholder": "Replace with",
- "prefixOptional": "Prefix (optional)",
- "suffixOptional": "Suffix (optional)",
- "useRegex": "Use Regex",
- "applyToSelectedNodes": "Apply to selected nodes",
- "selectFieldToApply": "Select a field to apply",
- "previewSummary": "Preview summary",
+ "title": "プロパティ",
+ "shape": "形状",
+ "color": "色",
+ "icon": "アイコン",
+ "rotation": "回転",
+ "transparency": "透明度",
+ "imageSettings": "画像設定",
+ "bulkShape": "一括形状",
+ "bulkColor": "一括色",
+ "bulkIcon": "一括アイコン",
+ "labelTransform": "ラベル変換",
+ "findReplace": "検索と置換",
+ "findLabel": "検索",
+ "findPlaceholder": "テキストを検索",
+ "replaceLabel": "置換",
+ "replacePlaceholder": "置換内容",
+ "prefixOptional": "プレフィックス(任意)",
+ "suffixOptional": "サフィックス(任意)",
+ "useRegex": "正規表現を使用",
+ "applyToSelectedNodes": "選択したノードに適用",
+ "selectFieldToApply": "適用するフィールドを選択",
+ "previewSummary": "プレビュー概要",
"selectionSummary": "選択の概要"
},
"saveStatus": {
@@ -346,8 +350,14 @@
"model": "モデル",
"apiKey": "APIキー",
"placeholder": "フローチャートを説明してください...",
- "settingsSubtitle": "Configure your preferred AI provider, model, and API key below.",
- "generateWithFlowpilot": "Flowpilotで生成"
+ "settingsSubtitle": "以下で希望の AI プロバイダー、モデル、API キーを設定してください。",
+ "generateWithFlowpilot": "Flowpilotで生成",
+ "howToGetKey": "APIキーの取得方法",
+ "openConsole": "コンソールを開く",
+ "pasteKeyStep": "上の入力欄に貼り付けてください。こちらには共有されません",
+ "customEndpointTitle": "カスタムエンドポイントとは?",
+ "customModelHint": "このエンドポイントで使う正確なモデル ID を入力してください",
+ "privacyTitle": "プライバシーと暗号化"
},
"playback": {
"title": "再生",
@@ -615,6 +625,13 @@
"edgeSyntax": "アーキテクチャのエッジ矢印 `-->`、`<--`、`<-->` と、`api:R --> L:db` のような側面修飾子を使用してください。",
"nodeSyntax": "有効なノード宣言を使用してください: `service id(icon)[Label]`、`group id[Label]`、`junction id[Label]`。",
"fallback": "自動回復を許可するにはアーキテクチャ厳格モードをオフにするか、診断を修正して再試行してください。"
+ },
+ "jumpToLine": "{{line}}行目に移動",
+ "diagnosticsGroup": {
+ "syntax": "構文の問題",
+ "identity": "識別子の問題",
+ "recovery": "リカバリー警告",
+ "general": "診断"
}
},
"layout": {
@@ -757,9 +774,7 @@
"infra": "Infra",
"openapi": "OpenAPI",
"code": "Code",
- "codebase": "リポジトリ",
- "mindmap": "マインドマップ",
- "markdown": "Markdown"
+ "codebase": "リポジトリ"
},
"title": "データからインポート",
"description": "コード、インフラ、SQL、または API 仕様を図に取り込みます。",
@@ -838,7 +853,9 @@
"edgeBundling": "兄弟エッジをバンドル",
"edgeBundlingDesc": "並行接続を共有レーンに保つ",
"architectureStrictMode": "アーキテクチャStrictモード",
- "architectureStrictModeDesc": "アーキテクチャ診断が回復/検証の問題を含む場合、Mermaidインポートをブロック"
+ "architectureStrictModeDesc": "アーキテクチャ診断が回復/検証の問題を含む場合、Mermaidインポートをブロック",
+ "miniMap": "ミニマップ",
+ "miniMapDesc": "右下にミニマップを表示"
},
"ai": {
"provider": "プロバイダー",
@@ -912,132 +929,227 @@
},
"models": {
"gemini": {
- "gemini-2": {
- "5-flash-lite": {
- "label": "Gemini 2.5 Flash Lite"
- },
- "5-flash": {
- "label": "Gemini 2.5 Flash"
- },
- "5-pro": {
- "label": "Gemini 2.5 Pro"
- }
- },
"gemini-3-flash": {
- "label": "Gemini 3 Flash"
+ "label": "Gemini 3 Flash",
+ "hint": "Frontier speed + intelligence",
+ "category": "Legacy",
+ "badge": "New"
},
"gemini-3-pro": {
- "label": "Gemini 3 Pro"
+ "label": "Gemini 3 Pro",
+ "hint": "Most powerful · Multimodal",
+ "category": "Legacy",
+ "badge": "New"
+ },
+ "gemini-2.5-flash-lite": {
+ "label": "2.5 Flash Lite",
+ "hint": "Fastest · Free tier default",
+ "category": "Speed",
+ "badge": "Default"
+ },
+ "gemini-2.5-flash": {
+ "label": "2.5 Flash",
+ "hint": "Best price/performance balance",
+ "category": "Speed"
+ },
+ "gemini-2.5-pro": {
+ "label": "2.5 Pro",
+ "hint": "Best reasoning · Complex diagrams",
+ "category": "Reasoning"
}
},
"openai": {
"gpt-5-mini": {
- "label": "GPT-5 Mini"
- },
- "gpt-5": {
- "2": {
- "label": "GPT-5.2"
- },
- "label": "GPT-5"
+ "label": "GPT-5 Mini",
+ "hint": "Fast · Cost-efficient",
+ "category": "Speed",
+ "badge": "Default"
},
"o4-mini": {
- "label": "O4-Mini"
+ "label": "O4-Mini",
+ "hint": "Advanced reasoning · Fast",
+ "category": "Reasoning",
+ "badge": "Reasoning"
},
"o3": {
- "label": "O3"
+ "label": "O3",
+ "hint": "Deep reasoning · Complex tasks",
+ "category": "Reasoning",
+ "badge": "Reasoning"
+ },
+ "gpt-5": {
+ "label": "GPT-5",
+ "hint": "Flagship model · Most capable",
+ "category": "Flagship"
+ },
+ "gpt-5.2": {
+ "label": "GPT-5.2",
+ "hint": "Latest update · Improved reasoning",
+ "category": "Reasoning",
+ "badge": "New"
}
},
"claude": {
"claude-haiku-4-5": {
- "label": "Claude Haiku 4.5"
+ "label": "Claude Haiku 4.5",
+ "hint": "Fastest · Most affordable",
+ "category": "Speed"
},
"claude-sonnet-4-5": {
- "label": "Claude Sonnet 4.5"
+ "label": "Claude Sonnet 4.5",
+ "hint": "Balanced intelligence & speed",
+ "category": "Flagship"
},
"claude-sonnet-4-6": {
- "label": "Claude Sonnet 4.6"
+ "label": "Claude Sonnet 4.6",
+ "hint": "Latest Sonnet · Best coding",
+ "category": "Flagship",
+ "badge": "Default"
},
"claude-opus-4-6": {
- "label": "Claude Opus 4.6"
+ "label": "Claude Opus 4.6",
+ "hint": "Most intelligent · 1M token context",
+ "category": "Reasoning",
+ "badge": "Flagship"
}
},
"groq": {
"meta-llama/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Free tier · Very fast",
+ "category": "Speed",
+ "badge": "Free"
},
"meta-llama/llama-4-maverick-17b-128e-instruct": {
- "label": "Llama 4 Maverick"
+ "label": "Llama 4 Maverick",
+ "hint": "More capable · Free tier",
+ "category": "Speed",
+ "badge": "Free"
},
"qwen/qwen3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
},
- "llama-3": {
- "3-70b-versatile": {
- "label": "Llama 3.3 70B"
- }
+ "llama-3.3-70b-versatile": {
+ "label": "Llama 3.3 70B Versatile",
+ "hint": "Versatile model",
+ "category": "Performance",
+ "badge": "Performance"
}
},
"nvidia": {
"meta/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Efficient · Multi-modal",
+ "category": "Speed"
},
"nvidia/nemotron-nano-12b-v2-vl": {
- "label": "Nemotron Nano 12B"
+ "label": "Nemotron Nano 12B",
+ "hint": "Lightweight · Vision-language · Fast",
+ "category": "Speed"
},
"deepseek/deepseek-v3-2": {
- "label": "DeepSeek V3-2"
+ "label": "DeepSeek V3-2",
+ "hint": "Latest · GPT-5 comparable",
+ "category": "Flagship",
+ "badge": "New"
},
"qwen/qwq-32b": {
- "label": "QwQ 32B"
+ "label": "QwQ 32B",
+ "hint": "Strong reasoning model",
+ "category": "Reasoning"
},
"moonshotai/kimi-k2-thinking": {
- "label": "Kimi K2 Thinking"
+ "label": "Kimi K2 Thinking",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"cerebras": {
"gpt-oss-120b": {
- "label": "GPT OSS 120B"
+ "label": "GPT OSS 120B",
+ "hint": "120B params · Fast on WSE-3",
+ "category": "Speed",
+ "badge": "Default"
},
"qwen-3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "2,403 tok/s · Industry fastest",
+ "category": "Speed",
+ "badge": "🚀 Fastest"
},
"qwen-3-235b-a22b": {
- "label": "Qwen 3 235B"
+ "label": "Qwen 3 235B",
+ "hint": "Flagship · Best quality",
+ "category": "Flagship",
+ "badge": "Flagship"
},
- "zai-glm-4": {
- "7": {
- "label": "Zai GLM 4.7"
- }
+ "zai-glm-4.7": {
+ "label": "Zai-GLM 4.7",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"mistral": {
"mistral-small-latest": {
- "label": "Mistral Small"
+ "label": "Mistral Small",
+ "hint": "Fast · Cost-efficient · 32k context",
+ "category": "Speed",
+ "badge": "Free"
},
"mistral-medium-latest": {
- "label": "Mistral Medium"
+ "label": "Mistral Medium",
+ "hint": "Balanced quality-cost · Best default",
+ "category": "Flagship",
+ "badge": "Default"
},
"mistral-large-latest": {
- "label": "Mistral Large"
+ "label": "Mistral Large",
+ "hint": "Most capable · 128k context · Flagship",
+ "category": "Flagship",
+ "badge": "Flagship"
},
"codestral-latest": {
- "label": "Codestral"
+ "label": "Codestral",
+ "hint": "Code-optimized · 256k context",
+ "category": "Coding",
+ "badge": "Code"
},
"pixtral-large-latest": {
- "label": "Pixtral Large"
+ "label": "Pixtral Large",
+ "hint": "Vision + reasoning · Multimodal",
+ "category": "Multimodal",
+ "badge": "Vision"
+ }
+ },
+ "custom": {
+ "custom": {
+ "label": "Custom Model",
+ "hint": "Enter your model ID below",
+ "category": "Custom"
}
}
},
"customEndpoints": {
"ollama": {
- "name": "Ollama"
+ "name": "Ollama",
+ "hint": "Local · Free"
},
"lmStudio": {
- "name": "LM Studio"
+ "name": "LM Studio",
+ "hint": "Local · Free"
},
"together": {
- "name": "Together AI"
+ "name": "Together AI",
+ "hint": "Cloud · Fast"
}
+ },
+ "byok": {
+ "dataPrivacy": "Your data never passes through our servers",
+ "control": "Full control over cost and rate limits",
+ "flexibility": "Switch providers anytime without re-linking",
+ "cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"brand": {
@@ -1078,19 +1190,7 @@
"analytics": {
"enableTitle": "匿名の起動分析",
"enableDescription": "匿名の基本的な利用データを共有します(任意)"
- },
- "search": "検索...",
- "appearance": "外観",
- "account": "アカウント",
- "about": "概要",
- "language": "言語",
- "theme": "テーマ",
- "fontSize": "フォントサイズ",
- "fontFamily": "フォント",
- "save": "保存",
- "cancel": "キャンセル",
- "reset": "リセット",
- "confirm": "確認"
+ }
},
"customNodes": {
"browserContent": "ブラウザコンテンツ",
@@ -1278,19 +1378,45 @@
"linkCopied": "共同編集リンクをコピーしました。",
"copyManual": "クリップボードへのアクセスがブロックされています。共有ダイアログからリンクを手動でコピーしてください。"
},
- "embed": "埋め込み",
- "embedCode": "埋め込みコード",
- "export": "エクスポート",
- "settings": "共有設定",
- "anyoneWithLink": "リンクを持つ任何人",
- "viewOnly": "表示のみ",
- "canEdit": "編集可能",
- "disable": "共有を無効化",
- "enable": "共有を有効化",
- "invite": "招待",
- "pending": "保留中",
- "revoke": "取り消し",
- "expired": "期限切れ"
+ "roomLink": "コラボレーションリンク",
+ "permissionsNote": "リンクを知っている人は誰でも参加できます。",
+ "permissionsNoteSecondary": "権限管理と永続的なバックエンド同期はまだ設定されていません。",
+ "viewerCount": {
+ "one": "このセッションに 1 人の閲覧者。",
+ "many": "このセッションに {{count}} 人の閲覧者。"
+ },
+ "mode": {
+ "realtime": {
+ "title": "リアルタイム同期が有効",
+ "body": "このリンクを開いたピアは、現在のピアトランスポートを通じてライブアップデートとカーソルを確認できます。"
+ },
+ "waiting": {
+ "title": "リアルタイム同期に接続中",
+ "body": "このキャンバスはライブピア同期の確立を試みています。失敗した場合、セッションはこのブラウザでローカルのみのままになります。"
+ },
+ "fallback": {
+ "title": "ローカルのみのコラボレーション",
+ "body": "バックエンドリレーまたはサポートされるピアトランスポートがない場合、このセッションは現在のブラウザランタイム外で永続的なマルチユーザーライブ同期を提供しません。"
+ }
+ },
+ "cache": {
+ "syncing": {
+ "title": "ルームのローカルキャッシュを同期中",
+ "body": "このブラウザは、ピア同期が完全に確立する前にIndexedDBにキャッシュされたルーム状態をまだ復元しています。"
+ },
+ "hydrated": {
+ "title": "ローカルキャッシュから復元しました",
+ "body": "このルームにはこのブラウザにローカルにキャッシュされた状態があったため、ピアが再接続する前にキャンバスを復元できました。"
+ },
+ "ready": {
+ "title": "ローカルキャッシュ準備完了",
+ "body": "このブラウザは、このデバイスでのリロードとオフライン回復のためにルームのローカルIndexedDBコピーを保持できます。"
+ },
+ "unavailable": {
+ "title": "ローカルルームキャッシュなし",
+ "body": "このコラボレーションセッションは現在、ブラウザIndexedDBルーム永続化を使用していません。"
+ }
+ }
},
"connectionPanel": {
"architecture": "アーキテクチャ",
@@ -1299,68 +1425,19 @@
"route": "ルート",
"appearance": "外観",
"condition": "条件",
- "deleteConnection": "接続を削除"
+ "deleteConnection": "接続を削除",
+ "style": "スタイル",
+ "actions": "アクション"
},
"sidebar": {
- "close": "サイドバーを閉じる",
- "searchPlaceholder": "検索...",
- "noResults": "結果なし",
- "nodes": "ノード",
- "components": "コンポーネント",
- "snippets": "スニペット",
- "addons": "アドオン"
+ "close": "サイドバーを閉じる"
},
"contextMenu": {
"label": "キャンバスコンテキストメニュー"
},
- "canvas": {
- "addNodeShortcut": "ノードを追加",
- "aiChatPlaceholder": "何かを作成...",
- "nodes": "ノード",
- "connections": "接続",
- "elements": "要素",
- "selection": "選択",
- "noSelection": "選択なし",
- "alignNodes": "ノードを整列",
- "distributeNodes": "ノードを分布",
- "alignLeft": "左揃え",
- "alignCenter": "中央揃え",
- "alignRight": "右揃え",
- "alignTop": "上揃え",
- "alignMiddle": "中間揃え",
- "alignBottom": "下揃え",
- "distributeHorizontally": "水平に分布",
- "distributeVertically": "垂直に分布",
- "zoomToFit": "フィットにズーム",
- "zoomToSelection": "選択にズーム",
- "locked": "ロック済み",
- "unlock": "ロック解除",
- "lock": "ロック",
- "bringToFront": "最前面へ",
- "sendToBack": "最背面へ",
- "group": "グループ化",
- "ungroup": "グループ解除"
- },
- "mindmap": {
- "addChild": "サブトピックを追加",
- "addSibling": "兄弟トピックを追加",
- "addParent": "親トピックを追加",
- "delete": "削除",
- "insertAfter": "後に挿入",
- "insertBefore": "前に挿入"
- },
+ "canvas": {},
+ "mindmap": {},
"aiModel": {
- "title": "AIモデル",
- "provider": "プロバイダー",
- "model": "モデル",
- "temperature": "温度",
- "maxTokens": "最大トークン数",
- "buttons": {
- "retry": "再試行",
- "stop": "停止",
- "clear": "クリア"
- },
- "thinking": "思考中...",
- "error": "AIエラー"
+ "buttons": {}
}
}
diff --git a/src/i18n/locales/tr/translation.json b/src/i18n/locales/tr/translation.json
index e63099a4..45a5ee57 100644
--- a/src/i18n/locales/tr/translation.json
+++ b/src/i18n/locales/tr/translation.json
@@ -167,7 +167,8 @@
"chooseFormat": "Biçim seç",
"speed": "Hız",
"resolution": "Çözünürlük",
- "transparentBackground": "Şeffaf arka plan"
+ "transparentBackground": "Şeffaf arka plan",
+ "headingFormat": "Dışa Aktarma Biçimi"
},
"import": {
"title": "İçe Aktar",
@@ -290,21 +291,21 @@
"hint": "Dışa aktarılmış bir yedeklemeniz veya başka bir kopyanız yoksa bu işlem geri alınamaz.",
"closeDialog": "Akış silme iletişim kutusunu kapat"
},
+ "homeEmptyTitle": "İlk akışınızı oluşturun",
+ "homeEmptySubtitle": "Kurumsal düzey mimariileri anında tasarlayın. Boş bir tuvalden başlayın, altyapınızı AI builderımızla tanımlayın veya hazır bir şablon kullanın.",
+ "homeBlankCanvas": "Boş Tuval",
+ "homeFlowpilotAI": "Flowpilot Yapay Zeka",
+ "homeTemplates": "Şablonlar",
+ "homeImportFile": "Veya mevcut bir dosyayı içe aktarın",
+ "suggestionAI": "Flowpilot Yapay Zeka",
"suggestionBlank": "Boş tuval",
"suggestionBlankDesc": "Doğrudan editöre geç",
- "suggestionAI": "Flowpilot Yapay Zeka",
"suggestionAIDesc": "Bir prompt ile başla",
"suggestionImport": "İçe Aktar",
"suggestionImportDesc": "Mevcut çalışmanı getir",
"suggestionTemplates": "Şablonlar",
"suggestionTemplatesDesc": "Kanıtlanmış bir kalıptan başla",
- "continueTitle": "Son eylemle devam et",
- "homeEmptyTitle": "İlk akışınızı oluşturun",
- "homeEmptySubtitle": "Kurumsal düzey mimariileri anında tasarlayın. Boş bir tuvalden başlayın, altyapınızı AI builderımızla tanımlayın veya hazır bir şablon kullanın.",
- "homeBlankCanvas": "Boş Tuval",
- "homeFlowpilotAI": "Flowpilot Yapay Zeka",
- "homeTemplates": "Şablonlar",
- "homeImportFile": "Veya mevcut bir dosyayı içe aktarın"
+ "continueTitle": "Son eylemle devam et"
},
"settings": {
"title": "Ayarlar",
@@ -644,6 +645,13 @@
"edgeSyntax": "`-->`, `<--` veya `<-->` mimari kenar oklarını ve `api:R --> L:db` gibi yan niteleyicileri kullanın.",
"nodeSyntax": "Geçerli düğüm tanımlarını kullanın: `service id(icon)[Label]`, `group id[Label]`, `junction id[Label]`.",
"fallback": "Otomatik toparlamaya izin vermek için Mimari Katı Modu kapatın veya tanıları düzeltip yeniden deneyin."
+ },
+ "jumpToLine": "{{line}} satırına git",
+ "diagnosticsGroup": {
+ "syntax": "Sözdizimi sorunları",
+ "identity": "Tanımlayıcı sorunları",
+ "recovery": "Kurtarma uyarıları",
+ "general": "Tanılama"
}
},
"layout": {
@@ -786,9 +794,7 @@
"infra": "Altyapı",
"openapi": "OpenAPI",
"code": "Kod",
- "codebase": "Depo",
- "mindmap": "Zihin Haritası",
- "markdown": "Markdown"
+ "codebase": "Depo"
},
"title": "Veriden içe aktar",
"description": "Kod, altyapı, SQL veya API özelliklerini diyagramınıza içe aktarın.",
@@ -867,7 +873,9 @@
"edgeBundling": "Kardeş Kenarları Grupla",
"edgeBundlingDesc": "Paralel bağlantıları paylaşılan şeritlerde tut",
"architectureStrictMode": "Mimari Katı Modu",
- "architectureStrictModeDesc": "Mimari tanılamalar kurtarma/doğrulama sorunları içerdiğinde Mermaid içe aktarmayı engelle"
+ "architectureStrictModeDesc": "Mimari tanılamalar kurtarma/doğrulama sorunları içerdiğinde Mermaid içe aktarmayı engelle",
+ "miniMap": "Mini Harita",
+ "miniMapDesc": "Sağ altta mini haritayı göster"
},
"ai": {
"provider": "Sağlayıcı",
@@ -947,17 +955,6 @@
"hint": "En güçlü · Çok modlu",
"category": "Eski",
"badge": "Yeni"
- },
- "gemini-2": {
- "5-flash-lite": {
- "label": "Gemini 2.5 Flash Lite"
- },
- "5-flash": {
- "label": "Gemini 2.5 Flash"
- },
- "5-pro": {
- "label": "Gemini 2.5 Pro"
- }
}
},
"openai": {
@@ -967,14 +964,6 @@
"category": "Hız",
"badge": "Varsayılan"
},
- "gpt-5": {
- "2": {
- "label": "GPT-5.2"
- },
- "label": "GPT-5",
- "hint": "Amiral gemisi · En yetenekli",
- "category": "Amiral Gemisi"
- },
"gpt-5.2": {
"label": "GPT-5.2",
"hint": "Son güncelleme · Geliştirilmiş akıl yürütme",
@@ -992,6 +981,11 @@
"hint": "Derin akıl yürütme · Karmaşık görevler",
"category": "Akıl Yürütme",
"badge": "Akıl Yürütme"
+ },
+ "gpt-5": {
+ "label": "GPT-5",
+ "hint": "Flagship model · Most capable",
+ "category": "Flagship"
}
},
"claude": {
@@ -1041,11 +1035,6 @@
"hint": "Çok yönlü model",
"category": "Performans",
"badge": "Performans"
- },
- "llama-3": {
- "3-70b-versatile": {
- "label": "Llama 3.3 70B"
- }
}
},
"nvidia": {
@@ -1099,11 +1088,6 @@
"label": "Zai-GLM 4.7",
"hint": "Gelişmiş akıl yürütme · Araç kullanımı",
"category": "Akıl Yürütme"
- },
- "zai-glm-4": {
- "7": {
- "label": "Zai GLM 4.7"
- }
}
},
"mistral": {
@@ -1226,19 +1210,7 @@
"analytics": {
"enableTitle": "Anonim başlatma analitiği",
"enableDescription": "Anonim temel kullanım verilerini paylaş (isteğe bağlı)"
- },
- "search": "Ara...",
- "appearance": "Görünüm",
- "account": "Hesap",
- "about": "Hakkında",
- "language": "Dil",
- "theme": "Tema",
- "fontSize": "Yazı boyutu",
- "fontFamily": "Yazı tipi",
- "save": "Kaydet",
- "cancel": "İptal",
- "reset": "Sıfırla",
- "confirm": "Onayla"
+ }
},
"customNodes": {
"browserContent": "Tarayıcı İçeriği",
@@ -1406,19 +1378,45 @@
"linkCopied": "İş birliği bağlantısı kopyalandı.",
"copyManual": "Pano erişimi engellendi. Bağlantıyı paylaşım diyaloğundan manuel olarak kopyalayın."
},
- "embed": "Göm",
- "embedCode": "Gömme kodu",
- "export": "Dışa aktar",
- "settings": "Paylaşım ayarları",
- "anyoneWithLink": "Bağlantısı olan herkes",
- "viewOnly": "Yalnızca görüntüleme",
- "canEdit": "Düzenleyebilir",
- "disable": "Paylaşımı kapat",
- "enable": "Paylaşımı aç",
- "invite": "Davet et",
- "pending": "Beklemede",
- "revoke": "İptal et",
- "expired": "Süresi doldu"
+ "roomLink": "İşbirliği Bağlantısı",
+ "permissionsNote": "Bağlantıya sahip herkes katılabilir.",
+ "permissionsNoteSecondary": "İzin kontrolleri ve kalıcı backend senkronizasyonu henüz yapılandırılmadı.",
+ "viewerCount": {
+ "one": "Bu oturumda 1 izleyici.",
+ "many": "Bu oturumda {{count}} izleyici."
+ },
+ "mode": {
+ "realtime": {
+ "title": "Eşzamanlı senkronizasyon etkin",
+ "body": "Bu bağlantıyı açan akranlar, mevcut aktarım üzerinden canlı güncellemeleri ve imleçleri görebilir."
+ },
+ "waiting": {
+ "title": "Eşzamanlı senkronizasyona bağlanılıyor",
+ "body": "Bu tuval hâlâ canlı eş akran senkronizasyonu kurmaya çalışıyor. Başarısız olursa oturum bu tarayıcıda yerel olarak kalır."
+ },
+ "fallback": {
+ "title": "Yalnızca yerel işbirliği",
+ "body": "Bir arka uç aktarıcı veya desteklenen eş aktarımı olmadan bu oturum, mevcut tarayıcı çalışma zamanı dışında kalıcı çoklu kullanıcı canlı senkronizasyonu sağlamaz."
+ }
+ },
+ "cache": {
+ "syncing": {
+ "title": "Yerel oda önbelleği senkronize ediliyor",
+ "body": "Bu tarayıcı, eş senkronizasyon tamamen yerleşmeden önce IndexedDB önb belleğe alınmış oda durumunu geri yüklemeye devam ediyor."
+ },
+ "hydrated": {
+ "title": "Yerel önbellekten kurtarıldı",
+ "body": "Bu odada bu tarayıcıda zaten yerel olarak önbelleğe alınmış bir durum vardı, bu nedenle eşler yeniden bağlanmadan önce tuval geri yüklenebildi."
+ },
+ "ready": {
+ "title": "Yerel önbellek hazır",
+ "body": "Bu tarayıcı, yeniden yükleme ve çevrimdışı kurtarma için odanın yerel bir IndexedDB kopyasını tutabilir."
+ },
+ "unavailable": {
+ "title": "Yerel oda önbelleği yok",
+ "body": "Bu işbirliği oturumu şu anda tarayıcı IndexedDB oda kalıcılığını kullanmıyor."
+ }
+ }
},
"connectionPanel": {
"architecture": "Mimari",
@@ -1427,68 +1425,19 @@
"route": "Yol",
"appearance": "Görünüm",
"condition": "Koşul",
- "deleteConnection": "Bağlantıyı sil"
+ "deleteConnection": "Bağlantıyı sil",
+ "style": "Stil",
+ "actions": "İşlemler"
},
"sidebar": {
- "close": "Kenar çubuğunu kapat",
- "searchPlaceholder": "Ara...",
- "noResults": "Sonuç yok",
- "nodes": "Düğümler",
- "components": "Bileşenler",
- "snippets": "Parçacıklar",
- "addons": "Eklentiler"
+ "close": "Kenar çubuğunu kapat"
},
"contextMenu": {
"label": "Tuval Bağlam Menüsü"
},
- "canvas": {
- "addNodeShortcut": "Düğüm ekle",
- "aiChatPlaceholder": "Bir şeyler oluştur...",
- "nodes": "Düğümler",
- "connections": "Bağlantılar",
- "elements": "Öğeler",
- "selection": "Seçim",
- "noSelection": "Seçim yok",
- "alignNodes": "Düğümleri hizala",
- "distributeNodes": "Düğümleri dağıt",
- "alignLeft": "Sola hizala",
- "alignCenter": "Merkeze hizala",
- "alignRight": "Sağa hizala",
- "alignTop": "Üste hizala",
- "alignMiddle": "Ortaya hizala",
- "alignBottom": "Alta hizala",
- "distributeHorizontally": "Yatay dağıt",
- "distributeVertically": "Dikey dağıt",
- "zoomToFit": "Sığdırmak için yakınlaştır",
- "zoomToSelection": "Seçime yakınlaştır",
- "locked": "Kilitli",
- "unlock": "Kilidi aç",
- "lock": "Kilitle",
- "bringToFront": "Öne getir",
- "sendToBack": "Arkaya gönder",
- "group": "Grupla",
- "ungroup": "Grubu kaldır"
- },
- "mindmap": {
- "addChild": "Alt konu ekle",
- "addSibling": "Kardeş konu ekle",
- "addParent": "Üst konu ekle",
- "delete": "Sil",
- "insertAfter": "Sonra ekle",
- "insertBefore": "Önce ekle"
- },
+ "canvas": {},
+ "mindmap": {},
"aiModel": {
- "title": "AI Modeli",
- "provider": "Sağlayıcı",
- "model": "Model",
- "temperature": "Sıcaklık",
- "maxTokens": "Maksimum belirteç",
- "buttons": {
- "retry": "Tekrar dene",
- "stop": "Durdur",
- "clear": "Temizle"
- },
- "thinking": "Düşünüyor...",
- "error": "AI hatası"
+ "buttons": {}
}
}
diff --git a/src/i18n/locales/zh/translation.json b/src/i18n/locales/zh/translation.json
index eee38fe7..b60cec1a 100644
--- a/src/i18n/locales/zh/translation.json
+++ b/src/i18n/locales/zh/translation.json
@@ -67,7 +67,10 @@
"distributeVertically": "垂直分布",
"group": "分组",
"upload": "上传",
- "rename": "重命名"
+ "rename": "重命名",
+ "export": "导出",
+ "exportDiagram": "导出图表",
+ "exportAs": "导出为"
},
"nav": {
"home": "首页",
@@ -123,13 +126,13 @@
"plantuml": "导出为 PlantUML",
"openflowdsl": "导出为 OpenFlow DSL",
"figma": "导出到 Figma",
- "hintTransparent4K": "Transparent (4K)",
- "hintWhiteBg4K": "White Background (4K)",
+ "hintTransparent4K": "透明 (4K)",
+ "hintWhiteBg4K": "白色背景 (4K)",
"jsonLabel": "JSON File",
- "hintDownload": "Download",
+ "hintDownload": "下载",
"openflowdslLabel": "{{appName}} DSL",
"hintClipboard": "复制",
- "figmaEditable": "Figma Editable",
+ "figmaEditable": "Figma 可编辑",
"exportDiagram": "Export Diagram",
"exportAs": "Export As",
"actionCopy": "复制",
@@ -144,27 +147,28 @@
"shareEmbed": "分享与嵌入",
"hintShareViewer": "此房间的邀请链接",
"hintShareEmbed": "只读查看链接",
- "readmeEmbed": "README Embed",
- "hintReadmeEmbed": "Copy Markdown snippet",
- "sectionImage": "Image",
+ "readmeEmbed": "README 嵌入",
+ "hintReadmeEmbed": "复制 Markdown 代码片段",
+ "sectionImage": "图片",
"sectionVideo": "视频",
"sectionCode": "代码",
"shareSection": "分享画布",
- "revealVideo": "Reveal Video",
- "hintRevealVideo": "Nodes fade in sequentially (WebM/MP4)",
- "revealGif": "Reveal GIF",
- "hintRevealGif": "Animated reveal for docs/social",
- "cinematicVideo": "Cinematic Build Video",
+ "revealVideo": "渐显视频",
+ "hintRevealVideo": "节点依次淡入 (WebM/MP4)",
+ "revealGif": "渐显 GIF",
+ "hintRevealGif": "适用于文档/社交媒体的动画渐显",
+ "cinematicVideo": "电影级构建视频",
"hintCinematicVideo": "Dark launch-style build",
- "cinematicGif": "Cinematic Build GIF",
- "hintCinematicGif": "Dark social-ready loop",
- "actionDownload": "Download",
+ "cinematicGif": "电影级构建 GIF",
+ "hintCinematicGif": "适用于社交媒体的深色循环",
+ "actionDownload": "下载",
"hintDocument": "文档",
"hintEditableSvg": "可编辑 SVG",
"chooseFormat": "选择格式",
"speed": "速度",
"resolution": "分辨率",
- "transparentBackground": "透明背景"
+ "transparentBackground": "透明背景",
+ "headingFormat": "导出格式"
},
"import": {
"title": "导入",
@@ -267,21 +271,21 @@
"hint": "除非您有导出的备份或另一份副本,否则此操作无法撤销。",
"closeDialog": "关闭删除流程对话框"
},
+ "homeEmptyTitle": "创建您的第一个流程",
+ "homeEmptySubtitle": "即时设计企业级架构。从空白画布开始,用我们的AI构建器描述您的基础设施,或使用定制模板。",
+ "homeBlankCanvas": "空白画布",
+ "homeFlowpilotAI": "流程领航人工智能",
+ "homeTemplates": "模板",
+ "homeImportFile": "或导入现有文件",
+ "suggestionAI": "Flowpilot 人工智能",
"suggestionBlank": "空白画布",
"suggestionBlankDesc": "直接进入编辑器",
- "suggestionAI": "流程领航人工智能",
"suggestionAIDesc": "从提示开始",
"suggestionImport": "导入",
"suggestionImportDesc": "导入现有工作",
"suggestionTemplates": "模板",
"suggestionTemplatesDesc": "从经过验证的模式开始",
- "continueTitle": "继续最近的操作",
- "homeEmptyTitle": "创建您的第一个流程",
- "homeEmptySubtitle": "即时设计企业级架构。从空白画布开始,用我们的AI构建器描述您的基础设施,或使用定制模板。",
- "homeBlankCanvas": "空白画布",
- "homeFlowpilotAI": "流程领航人工智能",
- "homeTemplates": "模板",
- "homeImportFile": "或导入现有文件"
+ "continueTitle": "继续最近的操作"
},
"settings": {
"title": "设置",
@@ -298,9 +302,9 @@
"about": "关于",
"brand": "品牌设置",
"privacy": "隐私",
- "themeLight": "Light",
- "themeDark": "Dark",
- "themeSystem": "System"
+ "themeLight": "浅色",
+ "themeDark": "深色",
+ "themeSystem": "跟随系统"
},
"shortcuts": {
"title": "键盘快捷键",
@@ -313,28 +317,28 @@
"help": "帮助"
},
"properties": {
- "title": "Properties",
- "shape": "Shape",
- "color": "Color",
- "icon": "Icon",
- "rotation": "Rotation",
- "transparency": "Transparency",
- "imageSettings": "Image Settings",
- "bulkShape": "Bulk Shape",
- "bulkColor": "Bulk Color",
- "bulkIcon": "Bulk Icon",
- "labelTransform": "Label Transform",
- "findReplace": "Find & Replace",
- "findLabel": "Find",
- "findPlaceholder": "Find text",
- "replaceLabel": "Replace",
- "replacePlaceholder": "Replace with",
- "prefixOptional": "Prefix (optional)",
- "suffixOptional": "Suffix (optional)",
- "useRegex": "Use Regex",
- "applyToSelectedNodes": "Apply to selected nodes",
- "selectFieldToApply": "Select a field to apply",
- "previewSummary": "Preview summary",
+ "title": "属性",
+ "shape": "形状",
+ "color": "颜色",
+ "icon": "图标",
+ "rotation": "旋转",
+ "transparency": "透明度",
+ "imageSettings": "图片设置",
+ "bulkShape": "批量形状",
+ "bulkColor": "批量颜色",
+ "bulkIcon": "批量图标",
+ "labelTransform": "标签变换",
+ "findReplace": "查找和替换",
+ "findLabel": "查找",
+ "findPlaceholder": "查找文本",
+ "replaceLabel": "替换",
+ "replacePlaceholder": "替换为",
+ "prefixOptional": "前缀(可选)",
+ "suffixOptional": "后缀(可选)",
+ "useRegex": "使用正则表达式",
+ "applyToSelectedNodes": "应用于所选节点",
+ "selectFieldToApply": "选择要应用的字段",
+ "previewSummary": "预览摘要",
"selectionSummary": "选择摘要"
},
"saveStatus": {
@@ -346,8 +350,14 @@
"model": "模型",
"apiKey": "API 密钥",
"placeholder": "描述你的流程图...",
- "settingsSubtitle": "Configure your preferred AI provider, model, and API key below.",
- "generateWithFlowpilot": "使用 Flowpilot 生成"
+ "settingsSubtitle": "在下方配置您首选的 AI 提供商、模型和 API 密钥。",
+ "generateWithFlowpilot": "使用 Flowpilot 生成",
+ "howToGetKey": "如何获取 API 密钥",
+ "openConsole": "打开控制台",
+ "pasteKeyStep": "将其粘贴到上方字段中,我们绝不会获取此信息",
+ "customEndpointTitle": "什么是自定义端点?",
+ "customModelHint": "输入此端点对应的准确模型 ID",
+ "privacyTitle": "隐私与加密"
},
"playback": {
"title": "回放",
@@ -615,6 +625,13 @@
"edgeSyntax": "使用架构边箭头 `-->`、`<--` 或 `<-->`,以及类似 `api:R --> L:db` 的侧向限定符。",
"nodeSyntax": "使用有效的节点声明:`service id(icon)[Label]`、`group id[Label]`、`junction id[Label]`。",
"fallback": "关闭架构严格模式以允许自动恢复,或修复诊断后重试。"
+ },
+ "jumpToLine": "跳转到第 {{line}} 行",
+ "diagnosticsGroup": {
+ "syntax": "语法问题",
+ "identity": "标识符问题",
+ "recovery": "恢复警告",
+ "general": "诊断信息"
}
},
"layout": {
@@ -757,9 +774,7 @@
"infra": "Infra",
"openapi": "OpenAPI",
"code": "Code",
- "codebase": "仓库",
- "mindmap": "思维导图",
- "markdown": "Markdown"
+ "codebase": "仓库"
},
"title": "从数据导入",
"description": "将代码、基础设施、SQL 或 API 规格导入到你的图表中。",
@@ -838,7 +853,9 @@
"edgeBundling": "捆绑兄弟边",
"edgeBundlingDesc": "将并行连接保持在共享车道上",
"architectureStrictMode": "架构严格模式",
- "architectureStrictModeDesc": "当架构诊断包含恢复/验证问题时阻止Mermaid导入"
+ "architectureStrictModeDesc": "当架构诊断包含恢复/验证问题时阻止Mermaid导入",
+ "miniMap": "小地图",
+ "miniMapDesc": "在右下角显示小地图"
},
"ai": {
"provider": "提供商",
@@ -912,132 +929,227 @@
},
"models": {
"gemini": {
- "gemini-2": {
- "5-flash-lite": {
- "label": "Gemini 2.5 Flash Lite"
- },
- "5-flash": {
- "label": "Gemini 2.5 Flash"
- },
- "5-pro": {
- "label": "Gemini 2.5 Pro"
- }
- },
"gemini-3-flash": {
- "label": "Gemini 3 Flash"
+ "label": "Gemini 3 Flash",
+ "hint": "Frontier speed + intelligence",
+ "category": "Legacy",
+ "badge": "New"
},
"gemini-3-pro": {
- "label": "Gemini 3 Pro"
+ "label": "Gemini 3 Pro",
+ "hint": "Most powerful · Multimodal",
+ "category": "Legacy",
+ "badge": "New"
+ },
+ "gemini-2.5-flash-lite": {
+ "label": "2.5 Flash Lite",
+ "hint": "Fastest · Free tier default",
+ "category": "Speed",
+ "badge": "Default"
+ },
+ "gemini-2.5-flash": {
+ "label": "2.5 Flash",
+ "hint": "Best price/performance balance",
+ "category": "Speed"
+ },
+ "gemini-2.5-pro": {
+ "label": "2.5 Pro",
+ "hint": "Best reasoning · Complex diagrams",
+ "category": "Reasoning"
}
},
"openai": {
"gpt-5-mini": {
- "label": "GPT-5 Mini"
- },
- "gpt-5": {
- "2": {
- "label": "GPT-5.2"
- },
- "label": "GPT-5"
+ "label": "GPT-5 Mini",
+ "hint": "Fast · Cost-efficient",
+ "category": "Speed",
+ "badge": "Default"
},
"o4-mini": {
- "label": "O4-Mini"
+ "label": "O4-Mini",
+ "hint": "Advanced reasoning · Fast",
+ "category": "Reasoning",
+ "badge": "Reasoning"
},
"o3": {
- "label": "O3"
+ "label": "O3",
+ "hint": "Deep reasoning · Complex tasks",
+ "category": "Reasoning",
+ "badge": "Reasoning"
+ },
+ "gpt-5": {
+ "label": "GPT-5",
+ "hint": "Flagship model · Most capable",
+ "category": "Flagship"
+ },
+ "gpt-5.2": {
+ "label": "GPT-5.2",
+ "hint": "Latest update · Improved reasoning",
+ "category": "Reasoning",
+ "badge": "New"
}
},
"claude": {
"claude-haiku-4-5": {
- "label": "Claude Haiku 4.5"
+ "label": "Claude Haiku 4.5",
+ "hint": "Fastest · Most affordable",
+ "category": "Speed"
},
"claude-sonnet-4-5": {
- "label": "Claude Sonnet 4.5"
+ "label": "Claude Sonnet 4.5",
+ "hint": "Balanced intelligence & speed",
+ "category": "Flagship"
},
"claude-sonnet-4-6": {
- "label": "Claude Sonnet 4.6"
+ "label": "Claude Sonnet 4.6",
+ "hint": "Latest Sonnet · Best coding",
+ "category": "Flagship",
+ "badge": "Default"
},
"claude-opus-4-6": {
- "label": "Claude Opus 4.6"
+ "label": "Claude Opus 4.6",
+ "hint": "Most intelligent · 1M token context",
+ "category": "Reasoning",
+ "badge": "Flagship"
}
},
"groq": {
"meta-llama/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Free tier · Very fast",
+ "category": "Speed",
+ "badge": "Free"
},
"meta-llama/llama-4-maverick-17b-128e-instruct": {
- "label": "Llama 4 Maverick"
+ "label": "Llama 4 Maverick",
+ "hint": "More capable · Free tier",
+ "category": "Speed",
+ "badge": "Free"
},
"qwen/qwen3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
},
- "llama-3": {
- "3-70b-versatile": {
- "label": "Llama 3.3 70B"
- }
+ "llama-3.3-70b-versatile": {
+ "label": "Llama 3.3 70B Versatile",
+ "hint": "Versatile model",
+ "category": "Performance",
+ "badge": "Performance"
}
},
"nvidia": {
"meta/llama-4-scout-17b-16e-instruct": {
- "label": "Llama 4 Scout"
+ "label": "Llama 4 Scout",
+ "hint": "Efficient · Multi-modal",
+ "category": "Speed"
},
"nvidia/nemotron-nano-12b-v2-vl": {
- "label": "Nemotron Nano 12B"
+ "label": "Nemotron Nano 12B",
+ "hint": "Lightweight · Vision-language · Fast",
+ "category": "Speed"
},
"deepseek/deepseek-v3-2": {
- "label": "DeepSeek V3-2"
+ "label": "DeepSeek V3-2",
+ "hint": "Latest · GPT-5 comparable",
+ "category": "Flagship",
+ "badge": "New"
},
"qwen/qwq-32b": {
- "label": "QwQ 32B"
+ "label": "QwQ 32B",
+ "hint": "Strong reasoning model",
+ "category": "Reasoning"
},
"moonshotai/kimi-k2-thinking": {
- "label": "Kimi K2 Thinking"
+ "label": "Kimi K2 Thinking",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"cerebras": {
"gpt-oss-120b": {
- "label": "GPT OSS 120B"
+ "label": "GPT OSS 120B",
+ "hint": "120B params · Fast on WSE-3",
+ "category": "Speed",
+ "badge": "Default"
},
"qwen-3-32b": {
- "label": "Qwen 3 32B"
+ "label": "Qwen 3 32B",
+ "hint": "2,403 tok/s · Industry fastest",
+ "category": "Speed",
+ "badge": "🚀 Fastest"
},
"qwen-3-235b-a22b": {
- "label": "Qwen 3 235B"
+ "label": "Qwen 3 235B",
+ "hint": "Flagship · Best quality",
+ "category": "Flagship",
+ "badge": "Flagship"
},
- "zai-glm-4": {
- "7": {
- "label": "Zai GLM 4.7"
- }
+ "zai-glm-4.7": {
+ "label": "Zai-GLM 4.7",
+ "hint": "Advanced reasoning · Tool use",
+ "category": "Reasoning"
}
},
"mistral": {
"mistral-small-latest": {
- "label": "Mistral Small"
+ "label": "Mistral Small",
+ "hint": "Fast · Cost-efficient · 32k context",
+ "category": "Speed",
+ "badge": "Free"
},
"mistral-medium-latest": {
- "label": "Mistral Medium"
+ "label": "Mistral Medium",
+ "hint": "Balanced quality-cost · Best default",
+ "category": "Flagship",
+ "badge": "Default"
},
"mistral-large-latest": {
- "label": "Mistral Large"
+ "label": "Mistral Large",
+ "hint": "Most capable · 128k context · Flagship",
+ "category": "Flagship",
+ "badge": "Flagship"
},
"codestral-latest": {
- "label": "Codestral"
+ "label": "Codestral",
+ "hint": "Code-optimized · 256k context",
+ "category": "Coding",
+ "badge": "Code"
},
"pixtral-large-latest": {
- "label": "Pixtral Large"
+ "label": "Pixtral Large",
+ "hint": "Vision + reasoning · Multimodal",
+ "category": "Multimodal",
+ "badge": "Vision"
+ }
+ },
+ "custom": {
+ "custom": {
+ "label": "Custom Model",
+ "hint": "Enter your model ID below",
+ "category": "Custom"
}
}
},
"customEndpoints": {
"ollama": {
- "name": "Ollama"
+ "name": "Ollama",
+ "hint": "Local · Free"
},
"lmStudio": {
- "name": "LM Studio"
+ "name": "LM Studio",
+ "hint": "Local · Free"
},
"together": {
- "name": "Together AI"
+ "name": "Together AI",
+ "hint": "Cloud · Fast"
}
+ },
+ "byok": {
+ "dataPrivacy": "Your data never passes through our servers",
+ "control": "Full control over cost and rate limits",
+ "flexibility": "Switch providers anytime without re-linking",
+ "cuttingEdge": "Access cutting-edge models as soon as they launch"
}
},
"brand": {
@@ -1078,19 +1190,7 @@
"analytics": {
"enableTitle": "匿名启动分析",
"enableDescription": "共享匿名的基础使用数据(可选)"
- },
- "search": "搜索...",
- "appearance": "外观",
- "account": "账户",
- "about": "关于",
- "language": "语言",
- "theme": "主题",
- "fontSize": "字体大小",
- "fontFamily": "字体",
- "save": "保存",
- "cancel": "取消",
- "reset": "重置",
- "confirm": "确认"
+ }
},
"customNodes": {
"browserContent": "浏览器内容",
@@ -1278,19 +1378,45 @@
"linkCopied": "协作链接已复制。",
"copyManual": "剪贴板访问被阻止。请从分享对话框中手动复制链接。"
},
- "embed": "嵌入",
- "embedCode": "嵌入代码",
- "export": "导出",
- "settings": "分享设置",
- "anyoneWithLink": "拥有链接的任何人",
- "viewOnly": "仅查看",
- "canEdit": "可以编辑",
- "disable": "关闭分享",
- "enable": "开启分享",
- "invite": "邀请",
- "pending": "待处理",
- "revoke": "撤销",
- "expired": "已过期"
+ "roomLink": "协作链接",
+ "permissionsNote": "任何拥有此链接的人都可以加入。",
+ "permissionsNoteSecondary": "权限控制和持久后端同步尚未配置。",
+ "viewerCount": {
+ "one": "此会话中有 1 位查看者。",
+ "many": "此会话中有 {{count}} 位查看者。"
+ },
+ "mode": {
+ "realtime": {
+ "title": "实时同步已激活",
+ "body": "打开此链接的对等方可以通过当前对等传输看到实时更新和光标。"
+ },
+ "waiting": {
+ "title": "正在连接实时同步",
+ "body": "此画布仍在尝试建立实时对等同步。如果无法建立,会话将在此浏览器中保持仅本地模式。"
+ },
+ "fallback": {
+ "title": "仅本地协作",
+ "body": "没有后端中继或受支持的对等传输,此会话无法在当前浏览器运行时之外提供持久的多用户实时同步。"
+ }
+ },
+ "cache": {
+ "syncing": {
+ "title": "本地房间缓存同步中",
+ "body": "此浏览器仍在恢复 IndexedDB 缓存的房间状态,然后再完全建立对等同步。"
+ },
+ "hydrated": {
+ "title": "已从本地缓存恢复",
+ "body": "此房间在此浏览器中已有本地缓存状态,因此画布可以在对等方重新连接之前恢复。"
+ },
+ "ready": {
+ "title": "本地缓存就绪",
+ "body": "此浏览器可以保存房间的本地 IndexedDB 副本,用于此设备上的重新加载和离线恢复。"
+ },
+ "unavailable": {
+ "title": "无本地房间缓存",
+ "body": "此协作会话当前未使用浏览器 IndexedDB 房间持久化。"
+ }
+ }
},
"connectionPanel": {
"architecture": "架构",
@@ -1299,68 +1425,19 @@
"route": "路由",
"appearance": "外观",
"condition": "条件",
- "deleteConnection": "删除连接"
+ "deleteConnection": "删除连接",
+ "style": "样式",
+ "actions": "操作"
},
"sidebar": {
- "close": "关闭侧边栏",
- "searchPlaceholder": "搜索...",
- "noResults": "无结果",
- "nodes": "节点",
- "components": "组件",
- "snippets": "代码片段",
- "addons": "插件"
+ "close": "关闭侧边栏"
},
"contextMenu": {
"label": "画布右键菜单"
},
- "canvas": {
- "addNodeShortcut": "添加节点",
- "aiChatPlaceholder": "创建一些内容...",
- "nodes": "节点",
- "connections": "连接",
- "elements": "元素",
- "selection": "选择",
- "noSelection": "无选择",
- "alignNodes": "对齐节点",
- "distributeNodes": "分布节点",
- "alignLeft": "左对齐",
- "alignCenter": "居中对齐",
- "alignRight": "右对齐",
- "alignTop": "顶部对齐",
- "alignMiddle": "中间对齐",
- "alignBottom": "底部对齐",
- "distributeHorizontally": "水平分布",
- "distributeVertically": "垂直分布",
- "zoomToFit": "缩放以适应",
- "zoomToSelection": "缩放到选择",
- "locked": "已锁定",
- "unlock": "解锁",
- "lock": "锁定",
- "bringToFront": "带到前面",
- "sendToBack": "发送到后面",
- "group": "分组",
- "ungroup": "取消分组"
- },
- "mindmap": {
- "addChild": "添加子主题",
- "addSibling": "添加同级主题",
- "addParent": "添加父主题",
- "delete": "删除",
- "insertAfter": "在后面插入",
- "insertBefore": "在前面插入"
- },
+ "canvas": {},
+ "mindmap": {},
"aiModel": {
- "title": "AI模型",
- "provider": "提供商",
- "model": "模型",
- "temperature": "温度",
- "maxTokens": "最大令牌数",
- "buttons": {
- "retry": "重试",
- "stop": "停止",
- "clear": "清除"
- },
- "thinking": "思考中...",
- "error": "AI错误"
+ "buttons": {}
}
}
diff --git a/src/lib/flowmindDSLParserV2.test.ts b/src/lib/flowmindDSLParserV2.test.ts
index ca1685a0..57414fef 100644
--- a/src/lib/flowmindDSLParserV2.test.ts
+++ b/src/lib/flowmindDSLParserV2.test.ts
@@ -76,7 +76,7 @@ describe('OpenFlow DSL V2 Parser', () => {
expect(p1?.data.quote).toBe('say "hello"');
});
- it('parses groups', () => {
+ it('ignores group wrappers and keeps inner nodes flat', () => {
const input = `
group "Backend" {
[process] api: API
@@ -85,9 +85,9 @@ describe('OpenFlow DSL V2 Parser', () => {
}
`;
const result = parseOpenFlowDslV2(input);
- expect(result.nodes).toHaveLength(3); // api, db, Backend group
+ expect(result.nodes).toHaveLength(2);
const api = result.nodes.find(n => n.id === 'api');
- expect(api?.parentId).toBeDefined();
+ expect(api?.parentId).toBeUndefined();
});
});
diff --git a/src/lib/flowmindDSLParserV2.ts b/src/lib/flowmindDSLParserV2.ts
index 59eba780..ab636984 100644
--- a/src/lib/flowmindDSLParserV2.ts
+++ b/src/lib/flowmindDSLParserV2.ts
@@ -38,7 +38,8 @@ const NODE_TYPE_MAP: Record = {
end: 'end',
system: 'custom',
note: 'annotation',
- section: 'section',
+ section: 'process',
+ group: 'process',
browser: 'browser',
mobile: 'mobile',
container: 'container', // New generic container
@@ -202,18 +203,7 @@ export function parseOpenFlowDslV2(input: string): DSLResult {
// 2. Groups Start: group "Label" {
const groupStartMatch = line.match(/^group\s+"?([^"{]+)"?\s*\{$/);
if (groupStartMatch) {
- const label = groupStartMatch[1];
- const id = `group-${dslNodes.length}`;
-
- dslNodes.push({
- id,
- type: 'group', // Generic group type, mapped to valid ReactFlow type later
- label,
- attributes: {},
- parentId: currentGroupStack.length > 0 ? currentGroupStack[currentGroupStack.length - 1] : undefined
- });
-
- currentGroupStack.push(id);
+ currentGroupStack.push(groupStartMatch[1]);
return;
}
@@ -288,7 +278,7 @@ export function parseOpenFlowDslV2(input: string): DSLResult {
type,
label,
attributes,
- parentId: currentGroupStack.length > 0 ? currentGroupStack[currentGroupStack.length - 1] : undefined
+ parentId: undefined
};
dslNodes.push(node);
diff --git a/src/lib/mermaidParser.ts b/src/lib/mermaidParser.ts
index 87d2dc1b..3b74e02b 100644
--- a/src/lib/mermaidParser.ts
+++ b/src/lib/mermaidParser.ts
@@ -70,7 +70,7 @@ function registerSectionNode(
state: ReturnType,
line: string
): boolean {
- const subgraphMatch = line.match(/^subgraph\s+([[]\w\s"'-]+)/i);
+ const subgraphMatch = line.match(/^subgraph\s+(.+)$/i);
const stateGroupMatch =
line.match(/^state\s+"([^"]+)"\s+as\s+(\w+)\s+\{/i) ||
line.match(/^state\s+(\w+)\s+\{/i);
@@ -78,27 +78,7 @@ function registerSectionNode(
if (!subgraphMatch && !stateGroupMatch) {
return false;
}
-
- let id: string;
- let label: string;
-
- if (subgraphMatch) {
- const col = subgraphMatch[1].trim();
- const titleMatch = col.match(/^(\w+)\s*\[(.+)\]$/);
- id = titleMatch ? titleMatch[1] : col.replace(/\s+/g, '_');
- label = titleMatch ? titleMatch[2] : col;
- } else {
- id = stateGroupMatch![2] || stateGroupMatch![1];
- label = stateGroupMatch![1];
- }
-
- state.nodesMap.set(id, {
- id,
- label,
- type: 'section',
- parentId: state.parentStack[state.parentStack.length - 1],
- });
- state.parentStack.push(id);
+ void state;
return true;
}
@@ -200,7 +180,9 @@ function buildMermaidParseModel(lines: string[]): MermaidParseModel {
}
if (line.match(/^end\s*$/i) || line === '}') {
- state.parentStack.pop();
+ if (state.parentStack.length > 0) {
+ state.parentStack.pop();
+ }
continue;
}
@@ -259,11 +241,6 @@ function createFlowNodes(model: MermaidParseModel): FlowNode[] {
flowNode = setNodeParent(flowNode, node.parentId);
}
- if (node.type === 'section') {
- flowNode.className = 'bg-slate-50/50 border-2 border-dashed border-slate-300 rounded-lg';
- flowNode.style = { width: 600, height: 400 };
- }
-
if (node.classes) {
node.classes.forEach((cls) => {
const styles = model.classDefs.get(cls);
diff --git a/src/services/aiService.ts b/src/services/aiService.ts
index 35a28dfd..48a748e5 100644
--- a/src/services/aiService.ts
+++ b/src/services/aiService.ts
@@ -1,4 +1,10 @@
-import { getSystemInstruction, ChatMessage, generateDiagramFromChat as generateDiagramFromChatGemini, chatWithDocsGemini } from './geminiService';
+import {
+ getSystemInstruction,
+ ChatMessage,
+ generateDiagramFromChat as generateDiagramFromChatGemini,
+ chatWithDocsGemini,
+ chatWithSystemInstructionGemini,
+} from './geminiService';
import { DEFAULT_MODELS, PROVIDER_BASE_URLS } from '@/config/aiProviders';
import { err, ok, type Result } from '@/lib/result';
import {
@@ -354,5 +360,62 @@ ${docsContext}
messages
);
}
+
+export async function chatWithFlowpilot(
+ history: ChatMessage[],
+ newMessage: string,
+ systemInstruction: string,
+ apiKeySetting?: string,
+ modelIdSetting?: string,
+ provider: AIProvider = 'gemini',
+ customBaseUrlSetting?: string,
+ onChunk?: (delta: string) => void,
+ signal?: AbortSignal,
+): Promise {
+ const apiKey = resolveApiKey(provider, apiKeySetting);
+ const modelId = resolveModelId(provider, modelIdSetting);
+
+ if (provider === 'gemini') {
+ return chatWithSystemInstructionGemini(
+ history,
+ newMessage,
+ systemInstruction,
+ apiKey,
+ modelId,
+ onChunk,
+ signal,
+ );
+ }
+
+ if (provider === 'claude') {
+ const messages: TextMessage[] = [
+ ...historyToMessages(history),
+ { role: 'user', content: newMessage },
+ ];
+ return callClaude(
+ apiKey,
+ modelId || DEFAULT_MODELS.claude,
+ messages,
+ systemInstruction,
+ onChunk,
+ signal,
+ );
+ }
+
+ const messages: TextMessage[] = [
+ { role: 'system', content: systemInstruction },
+ ...historyToMessages(history),
+ { role: 'user', content: newMessage },
+ ];
+
+ return callOpenAICompatible(
+ resolveOpenAICompatibleBaseUrl(provider, customBaseUrlSetting),
+ apiKey,
+ modelId || DEFAULT_MODELS[provider],
+ messages,
+ onChunk,
+ signal,
+ );
+}
export type { ChatMessage };
export { parseClaudeContent, parseOpenAICompatibleContent };
diff --git a/src/services/export/cinematicExport.ts b/src/services/export/cinematicExport.ts
new file mode 100644
index 00000000..d4e06b04
--- /dev/null
+++ b/src/services/export/cinematicExport.ts
@@ -0,0 +1,66 @@
+export type CinematicExportFormat = 'cinematic-video';
+export type CinematicExportSpeed = 'slow' | 'normal' | 'fast';
+export type CinematicExportResolution = '720p' | '1080p' | '4k';
+export type CinematicThemeMode = 'light' | 'dark';
+
+export interface CinematicExportRequest {
+ format: CinematicExportFormat;
+ speed: CinematicExportSpeed;
+ resolution: CinematicExportResolution;
+ themeMode: CinematicThemeMode;
+}
+
+export interface CinematicResolutionPreset {
+ maxDimension: number;
+ pixelRatio: number;
+ label: string;
+}
+
+export function getCinematicSpeedMultiplier(speed: CinematicExportSpeed): number {
+ if (speed === 'slow') {
+ return 0.72;
+ }
+
+ if (speed === 'fast') {
+ return 1.7;
+ }
+
+ return 1;
+}
+
+export function getCinematicResolutionPreset(
+ resolution: CinematicExportResolution
+): CinematicResolutionPreset {
+ if (resolution === '720p') {
+ return {
+ maxDimension: 1280,
+ pixelRatio: 1.5,
+ label: '720p',
+ };
+ }
+
+ if (resolution === '4k') {
+ return {
+ maxDimension: 3840,
+ pixelRatio: 2,
+ label: '4k',
+ };
+ }
+
+ return {
+ maxDimension: 1920,
+ pixelRatio: 2,
+ label: '1080p',
+ };
+}
+
+export function createDefaultCinematicExportRequest(
+ themeMode: CinematicThemeMode
+): CinematicExportRequest {
+ return {
+ format: 'cinematic-video',
+ speed: 'normal',
+ resolution: '1080p',
+ themeMode,
+ };
+}
diff --git a/src/services/export/cinematicExportTheme.test.ts b/src/services/export/cinematicExportTheme.test.ts
index 9907c5f2..c7662869 100644
--- a/src/services/export/cinematicExportTheme.test.ts
+++ b/src/services/export/cinematicExportTheme.test.ts
@@ -3,6 +3,7 @@ import {
CINEMATIC_EXPORT_FALLBACK_COLOR,
CINEMATIC_EXPORT_SURFACE_BACKGROUND,
paintCinematicExportBackground,
+ resolveCinematicExportTheme,
} from './cinematicExportTheme';
function createGradient() {
@@ -27,7 +28,7 @@ describe('cinematicExportTheme', () => {
createRadialGradient: vi.fn(() => radialGradient),
};
- paintCinematicExportBackground(context, 1200, 630);
+ paintCinematicExportBackground(context, 1200, 630, 'light');
expect(context.createLinearGradient).toHaveBeenCalledWith(0, 0, 0, 630);
expect(context.createRadialGradient).toHaveBeenCalled();
@@ -35,4 +36,11 @@ describe('cinematicExportTheme', () => {
expect(radialGradient.addColorStop).toHaveBeenCalledTimes(2);
expect(context.fillRect).toHaveBeenCalledTimes(2);
});
+
+ it('returns a dark export theme when requested', () => {
+ const theme = resolveCinematicExportTheme('dark');
+
+ expect(theme.mode).toBe('dark');
+ expect(theme.fallbackColor).not.toBe(CINEMATIC_EXPORT_FALLBACK_COLOR);
+ });
});
diff --git a/src/services/export/cinematicExportTheme.ts b/src/services/export/cinematicExportTheme.ts
index f75f32ad..3305ec48 100644
--- a/src/services/export/cinematicExportTheme.ts
+++ b/src/services/export/cinematicExportTheme.ts
@@ -1,7 +1,10 @@
-export const CINEMATIC_EXPORT_SURFACE_BACKGROUND =
- 'radial-gradient(circle at top, rgba(59,130,246,0.14), transparent 42%), linear-gradient(180deg, #f8fbff 0%, #eef5ff 52%, #f8fafc 100%)';
+import type { CinematicThemeMode } from './cinematicExport';
-export const CINEMATIC_EXPORT_FALLBACK_COLOR = '#f8fbff';
+export interface CinematicExportTheme {
+ mode: CinematicThemeMode;
+ surfaceBackground: string;
+ fallbackColor: string;
+}
export interface BackgroundPaintContext {
fillStyle: string | CanvasGradient | CanvasPattern;
@@ -17,18 +20,34 @@ export interface BackgroundPaintContext {
) => CanvasGradient;
}
+const LIGHT_THEME: CinematicExportTheme = {
+ mode: 'light',
+ surfaceBackground:
+ 'radial-gradient(circle at top, rgba(59,130,246,0.14), transparent 42%), linear-gradient(180deg, #f8fbff 0%, #eef5ff 52%, #f8fafc 100%)',
+ fallbackColor: '#f8fbff',
+};
+
+const DARK_THEME: CinematicExportTheme = {
+ mode: 'dark',
+ surfaceBackground:
+ 'radial-gradient(circle at top, rgba(56,189,248,0.16), transparent 44%), linear-gradient(180deg, #06111f 0%, #0b1728 54%, #111c2d 100%)',
+ fallbackColor: '#0b1728',
+};
+
+export const CINEMATIC_EXPORT_SURFACE_BACKGROUND = LIGHT_THEME.surfaceBackground;
+export const CINEMATIC_EXPORT_FALLBACK_COLOR = LIGHT_THEME.fallbackColor;
+
+export function resolveCinematicExportTheme(mode: CinematicThemeMode): CinematicExportTheme {
+ return mode === 'dark' ? DARK_THEME : LIGHT_THEME;
+}
+
export function paintCinematicExportBackground(
context: BackgroundPaintContext,
width: number,
height: number,
+ mode: CinematicThemeMode,
): void {
const linearGradient = context.createLinearGradient(0, 0, 0, height);
- linearGradient.addColorStop(0, '#f8fbff');
- linearGradient.addColorStop(0.52, '#eef5ff');
- linearGradient.addColorStop(1, '#f8fafc');
- context.fillStyle = linearGradient;
- context.fillRect(0, 0, width, height);
-
const radialGradient = context.createRadialGradient(
width * 0.5,
0,
@@ -37,8 +56,23 @@ export function paintCinematicExportBackground(
0,
Math.max(width, height) * 0.42,
);
- radialGradient.addColorStop(0, 'rgba(59,130,246,0.14)');
- radialGradient.addColorStop(1, 'rgba(59,130,246,0)');
+
+ if (mode === 'dark') {
+ linearGradient.addColorStop(0, '#06111f');
+ linearGradient.addColorStop(0.54, '#0b1728');
+ linearGradient.addColorStop(1, '#111c2d');
+ radialGradient.addColorStop(0, 'rgba(56,189,248,0.16)');
+ radialGradient.addColorStop(1, 'rgba(56,189,248,0)');
+ } else {
+ linearGradient.addColorStop(0, '#f8fbff');
+ linearGradient.addColorStop(0.52, '#eef5ff');
+ linearGradient.addColorStop(1, '#f8fafc');
+ radialGradient.addColorStop(0, 'rgba(59,130,246,0.14)');
+ radialGradient.addColorStop(1, 'rgba(59,130,246,0)');
+ }
+
+ context.fillStyle = linearGradient;
+ context.fillRect(0, 0, width, height);
context.fillStyle = radialGradient;
context.fillRect(0, 0, width, height);
}
diff --git a/src/services/export/cinematicRenderState.test.ts b/src/services/export/cinematicRenderState.test.ts
index 5d57059a..06f21868 100644
--- a/src/services/export/cinematicRenderState.test.ts
+++ b/src/services/export/cinematicRenderState.test.ts
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import type { FlowEdge } from '@/lib/types';
+import type { CinematicExportRequest } from './cinematicExport';
import { buildCinematicBuildPlan } from './cinematicBuildPlan';
import { buildCinematicTimeline, getCinematicExportPreset, resolveCinematicRenderState } from './cinematicRenderState';
@@ -14,10 +15,17 @@ const edges = [
{ id: 'bc', source: 'b', target: 'c' },
] as FlowEdge[];
+const videoRequest: CinematicExportRequest = {
+ format: 'cinematic-video',
+ speed: 'normal',
+ resolution: '1080p',
+ themeMode: 'light',
+};
+
describe('cinematic render state', () => {
it('keeps the screen empty during the intro hold', () => {
const plan = buildCinematicBuildPlan([...nodes], edges);
- const timeline = buildCinematicTimeline(plan, getCinematicExportPreset('cinematic-video'));
+ const timeline = buildCinematicTimeline(plan, getCinematicExportPreset(videoRequest));
const state = resolveCinematicRenderState(timeline, edges, 0);
@@ -28,7 +36,7 @@ describe('cinematic render state', () => {
it('activates the lead edge before the target node fade', () => {
const plan = buildCinematicBuildPlan([...nodes], edges);
- const timeline = buildCinematicTimeline(plan, getCinematicExportPreset('cinematic-video'));
+ const timeline = buildCinematicTimeline(plan, getCinematicExportPreset(videoRequest));
const secondSegment = timeline.segments[1];
const midpoint = Math.round(((secondSegment.edgeGrowStartMs ?? 0) + (secondSegment.edgeGrowEndMs ?? 0)) / 2);
@@ -40,4 +48,21 @@ describe('cinematic render state', () => {
expect(state.visibleNodeIds.has('a')).toBe(true);
expect(state.visibleNodeIds.has('b')).toBe(false);
});
+
+ it('uses different timeline presets for speed and resolution choices', () => {
+ const slow4kPreset = getCinematicExportPreset({
+ ...videoRequest,
+ speed: 'slow',
+ resolution: '4k',
+ });
+ const fast720Preset = getCinematicExportPreset({
+ ...videoRequest,
+ speed: 'fast',
+ resolution: '720p',
+ });
+
+ expect(slow4kPreset.maxDimension).toBeGreaterThan(fast720Preset.maxDimension);
+ expect(slow4kPreset.pixelRatio).toBeGreaterThanOrEqual(fast720Preset.pixelRatio);
+ expect(slow4kPreset.edgeGrowMs).toBeGreaterThan(fast720Preset.edgeGrowMs);
+ });
});
diff --git a/src/services/export/cinematicRenderState.ts b/src/services/export/cinematicRenderState.ts
index 3a3222ef..8313c678 100644
--- a/src/services/export/cinematicRenderState.ts
+++ b/src/services/export/cinematicRenderState.ts
@@ -1,12 +1,17 @@
import type { FlowEdge } from '@/lib/types';
+import {
+ getCinematicResolutionPreset,
+ getCinematicSpeedMultiplier,
+ type CinematicExportRequest,
+ type CinematicThemeMode,
+} from './cinematicExport';
import type {
CinematicBuildPlan,
CinematicBuildSegment,
- CinematicExportKind,
} from './cinematicBuildPlan';
export interface CinematicExportPreset {
- kind: CinematicExportKind;
+ kind: CinematicExportRequest['format'];
fps: number;
maxDimension: number;
pixelRatio: number;
@@ -22,7 +27,7 @@ export interface CinematicExportPreset {
export interface CinematicRenderState {
active: boolean;
- backgroundMode: 'light';
+ backgroundMode: CinematicThemeMode;
visibleNodeIds: ReadonlySet;
builtEdgeIds: ReadonlySet;
visibleEdgeIds: ReadonlySet;
@@ -51,7 +56,6 @@ export interface CinematicTimeline {
}
const EMPTY_SET = new Set();
-const LIGHT_BACKGROUND_MODE = 'light' as const;
function clamp01(value: number): number {
return Math.max(0, Math.min(1, value));
@@ -70,7 +74,7 @@ function easeOutQuart(value: number): number {
function createInactiveSegmentRenderState(): CinematicRenderState {
return {
active: true,
- backgroundMode: LIGHT_BACKGROUND_MODE,
+ backgroundMode: 'light',
visibleNodeIds: EMPTY_SET,
builtEdgeIds: EMPTY_SET,
visibleEdgeIds: EMPTY_SET,
@@ -83,39 +87,25 @@ function createInactiveSegmentRenderState(): CinematicRenderState {
}
export function getCinematicExportPreset(
- kind: CinematicExportKind,
- speedMultiplier = 1,
- maxDimension?: number
+ request: Pick
): CinematicExportPreset {
- if (kind === 'cinematic-gif') {
- return {
- kind,
- fps: 8,
- maxDimension: maxDimension ?? 960,
- pixelRatio: 1,
- introHoldMs: Math.round(180 / speedMultiplier),
- rootNodeFadeMs: Math.round(260 / speedMultiplier),
- edgeGrowMs: Math.round(360 / speedMultiplier),
- targetNodeFadeMs: Math.round(240 / speedMultiplier),
- settleMs: Math.round(120 / speedMultiplier),
- finalHoldMs: Math.round(480 / speedMultiplier),
- maxFrames: 140,
- speedMultiplier,
- };
- }
+ const speedMultiplier = getCinematicSpeedMultiplier(request.speed);
+ const resolutionPreset = getCinematicResolutionPreset(request.resolution);
+ const isHighResolution = request.resolution === '4k';
+ const isLowResolution = request.resolution === '720p';
return {
- kind,
- fps: 20,
- maxDimension: maxDimension ?? 1600,
- pixelRatio: 1,
+ kind: request.format,
+ fps: isHighResolution ? 24 : isLowResolution ? 18 : 20,
+ maxDimension: resolutionPreset.maxDimension,
+ pixelRatio: resolutionPreset.pixelRatio,
introHoldMs: Math.round(200 / speedMultiplier),
rootNodeFadeMs: Math.round(300 / speedMultiplier),
edgeGrowMs: Math.round(500 / speedMultiplier),
targetNodeFadeMs: Math.round(260 / speedMultiplier),
settleMs: Math.round(150 / speedMultiplier),
finalHoldMs: Math.round(640 / speedMultiplier),
- maxFrames: 280,
+ maxFrames: isHighResolution ? 320 : isLowResolution ? 220 : 280,
speedMultiplier,
};
}
@@ -201,7 +191,8 @@ function buildVisibleEdgeIds(visibleNodeIds: Set, edges: FlowEdge[]): Se
export function resolveCinematicRenderState(
timeline: CinematicTimeline,
edges: FlowEdge[],
- timeMs: number
+ timeMs: number,
+ backgroundMode: CinematicThemeMode = 'light'
): CinematicRenderState {
if (timeline.plan.segments.length === 0) {
return createInactiveSegmentRenderState();
@@ -219,6 +210,7 @@ export function resolveCinematicRenderState(
if (clampedTimeMs < timeline.preset.introHoldMs) {
return {
...createInactiveSegmentRenderState(),
+ backgroundMode,
visibleNodeIds,
};
}
@@ -272,7 +264,7 @@ export function resolveCinematicRenderState(
return {
active: true,
- backgroundMode: LIGHT_BACKGROUND_MODE,
+ backgroundMode,
visibleNodeIds,
builtEdgeIds: new Set(visibleEdgeIds),
visibleEdgeIds: activeEdgeId ? new Set([...visibleEdgeIds, activeEdgeId]) : visibleEdgeIds,
diff --git a/src/services/export/mermaid/stateDiagramMermaid.ts b/src/services/export/mermaid/stateDiagramMermaid.ts
index f91f5a05..3cc9e7e6 100644
--- a/src/services/export/mermaid/stateDiagramMermaid.ts
+++ b/src/services/export/mermaid/stateDiagramMermaid.ts
@@ -14,19 +14,16 @@ function escapeStateLabel(label: string): string {
}
function isStateDiagramNodeType(type: string | undefined): boolean {
- return type === 'state' || type === 'start' || type === 'section' || type === 'process';
+ return type === 'state' || type === 'start' || type === 'process';
}
export function looksLikeStateDiagram(nodes: FlowNode[]): boolean {
if (nodes.length === 0) return false;
const hasStateStartNode = nodes.some((node) => node.id.startsWith('state_start_'));
const hasExplicitStateNode = nodes.some((node) => node.type === 'state');
- const sectionIds = new Set(
- nodes.filter((node) => node.type === 'section').map((node) => node.id)
- );
const hasCompositeParenting = nodes.some((node) => {
const parentId = getNodeParentId(node);
- return parentId.length > 0 && sectionIds.has(parentId);
+ return parentId.length > 0;
});
if (!hasStateStartNode && !hasExplicitStateNode && !hasCompositeParenting) {
@@ -101,39 +98,58 @@ export function toStateDiagramMermaid(nodes: FlowNode[], edges: FlowEdge[]): str
function emitNodeDeclaration(node: FlowNode, depth: number): void {
const indent = ' '.repeat(depth);
+ const children = sortNodesByPosition(childrenByParentId.get(node.id) ?? []);
if (node.type === 'start') {
return;
}
- if (node.type === 'section') {
- const label = String(node.data.label || node.id).trim() || node.id;
- if (label === node.id) {
- lines.push(`${indent}state ${node.id} {`);
- } else {
- lines.push(`${indent}state "${escapeStateLabel(label)}" as ${node.id} {`);
- }
-
- const children = sortNodesByPosition(childrenByParentId.get(node.id) ?? []);
- children.forEach((child) => emitNodeDeclaration(child, depth + 1));
- lines.push(`${indent}}`);
+ const label = String(node.data.label || node.id).trim() || node.id;
+ if (children.length === 0) {
+ lines.push(`${indent}state "${escapeStateLabel(label)}" as ${node.id}`);
return;
}
- const label = String(node.data.label || node.id).trim() || node.id;
- lines.push(`${indent}state "${escapeStateLabel(label)}" as ${node.id}`);
- }
+ lines.push(`${indent}state ${node.id} {`);
+ children.forEach((childNode) => emitNodeDeclaration(childNode, depth + 1));
+ edges.forEach((edge) => {
+ const sourceNode = nodeById.get(edge.source);
+ const targetNode = nodeById.get(edge.target);
+ if (!sourceNode && !targetNode) return;
+
+ const sourceParentId = sourceNode ? getNodeParentId(sourceNode) : '';
+ const targetParentId = targetNode ? getNodeParentId(targetNode) : '';
+ const shouldEmitInsideParent =
+ (sourceParentId === node.id && (targetParentId === node.id || edge.target.startsWith('state_start_'))) ||
+ (targetParentId === node.id && edge.source.startsWith('state_start_'));
+
+ if (!shouldEmitInsideParent) {
+ return;
+ }
- topLevelNodes.forEach((node) => emitNodeDeclaration(node, 1));
+ const sourceToken = toStateNodeToken(edge.source, startNodeIds);
+ const targetToken = toStateNodeToken(edge.target, startNodeIds);
+ const connector = resolveStateConnector(edge);
+ const edgeLabel = typeof edge.label === 'string' ? edge.label.trim() : '';
+ const suffix = edgeLabel ? ` : ${edgeLabel}` : '';
+ lines.push(`${' '.repeat(depth + 1)}${sourceToken} ${connector} ${targetToken}${suffix}`);
+ });
+ lines.push(`${indent}}`);
+ }
const startNodeIds = new Set(
nodes.filter((node) => node.type === 'start').map((node) => node.id)
);
+ topLevelNodes.forEach((node) => emitNodeDeclaration(node, 1));
edges.forEach((edge) => {
const sourceNode = nodeById.get(edge.source);
const targetNode = nodeById.get(edge.target);
if (!sourceNode || !targetNode) return;
+ if (getNodeParentId(sourceNode) && getNodeParentId(sourceNode) === getNodeParentId(targetNode)) {
+ return;
+ }
+
const sourceToken = toStateNodeToken(edge.source, startNodeIds);
const targetToken = toStateNodeToken(edge.target, startNodeIds);
const connector = resolveStateConnector(edge);
diff --git a/src/services/flowpilot/assetGrounding.test.ts b/src/services/flowpilot/assetGrounding.test.ts
new file mode 100644
index 00000000..898fb2f9
--- /dev/null
+++ b/src/services/flowpilot/assetGrounding.test.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it, vi } from 'vitest';
+
+const loadDomainAssetSuggestions = vi.fn();
+
+vi.mock('@/services/assetCatalog', () => ({
+ loadDomainAssetSuggestions,
+}));
+
+describe('flowpilot asset grounding', () => {
+ it('returns provider-backed matches for common services', async () => {
+ loadDomainAssetSuggestions.mockImplementation(async (category: string, options: { query?: string }) => {
+ if (category === 'aws' && options.query === 'S3') {
+ return [
+ {
+ id: 'aws-official-starter-v1:storage-simple-storage-service',
+ category: 'aws',
+ label: 'S3',
+ description: 'AWS Storage',
+ icon: 'Box',
+ color: 'amber',
+ archIconPackId: 'aws-official-starter-v1',
+ archIconShapeId: 'storage-simple-storage-service',
+ },
+ ];
+ }
+
+ return [];
+ });
+
+ const { groundFlowpilotAssets } = await import('./assetGrounding');
+ const result = await groundFlowpilotAssets('Create an AWS diagram with S3 storage');
+
+ expect(result[0]?.label).toBe('S3');
+ expect(result[0]?.archIconPackId).toBe('aws-official-starter-v1');
+ });
+});
diff --git a/src/services/flowpilot/assetGrounding.ts b/src/services/flowpilot/assetGrounding.ts
new file mode 100644
index 00000000..98877e7f
--- /dev/null
+++ b/src/services/flowpilot/assetGrounding.ts
@@ -0,0 +1,130 @@
+import type { DomainLibraryCategory, DomainLibraryItem } from '@/services/domainLibrary';
+import { loadDomainAssetSuggestions } from '@/services/assetCatalog';
+import type { AssetGroundingMatch } from './types';
+
+const ALL_GROUNDING_CATEGORIES: DomainLibraryCategory[] = [
+ 'aws',
+ 'azure',
+ 'gcp',
+ 'cncf',
+ 'developer',
+ 'icons',
+];
+
+const SERVICE_ALIASES: Array<{ query: string; categories?: DomainLibraryCategory[] }> = [
+ { query: 'API Gateway', categories: ['aws'] },
+ { query: 'Lambda', categories: ['aws'] },
+ { query: 'S3', categories: ['aws'] },
+ { query: 'RDS', categories: ['aws'] },
+ { query: 'ElastiCache', categories: ['aws'] },
+ { query: 'Cognito', categories: ['aws'] },
+ { query: 'Azure Functions', categories: ['azure'] },
+ { query: 'Azure SQL', categories: ['azure'] },
+ { query: 'Storage Account', categories: ['azure'] },
+ { query: 'API Management', categories: ['azure'] },
+ { query: 'Cloud Run', categories: ['gcp'] },
+ { query: 'Cloud SQL', categories: ['gcp'] },
+ { query: 'Cloud Storage', categories: ['gcp'] },
+ { query: 'Kubernetes', categories: ['cncf'] },
+ { query: 'Ingress', categories: ['cncf'] },
+ { query: 'Redis' },
+ { query: 'Postgres' },
+ { query: 'Queue' },
+ { query: 'Database' },
+];
+
+function scoreMatch(item: DomainLibraryItem, query: string): number {
+ const normalizedQuery = query.toLowerCase();
+ const label = item.label.toLowerCase();
+ const description = item.description.toLowerCase();
+ const category = (item.providerShapeCategory || '').toLowerCase();
+
+ if (label === normalizedQuery) {
+ return 0.99;
+ }
+
+ if (label.includes(normalizedQuery)) {
+ return 0.93;
+ }
+
+ if (description.includes(normalizedQuery) || category.includes(normalizedQuery)) {
+ return 0.82;
+ }
+
+ return 0.65;
+}
+
+function toGroundingMatch(item: DomainLibraryItem, query: string): AssetGroundingMatch {
+ return {
+ id: item.id,
+ label: item.label,
+ description: item.description,
+ category: item.category,
+ archProvider: item.category,
+ archResourceType: item.archResourceType || 'service',
+ archIconPackId: item.archIconPackId,
+ archIconShapeId: item.archIconShapeId,
+ providerShapeCategory: item.providerShapeCategory,
+ confidence: scoreMatch(item, query),
+ reasoning: item.archIconPackId
+ ? `Matched "${query}" to a provider-backed asset in the local ${item.category.toUpperCase()} catalog.`
+ : `Matched "${query}" to a reusable local asset.`,
+ };
+}
+
+function inferQueriesFromPrompt(prompt: string): Array<{ query: string; categories?: DomainLibraryCategory[] }> {
+ const matches = SERVICE_ALIASES.filter((entry) => prompt.toLowerCase().includes(entry.query.toLowerCase()));
+ if (matches.length > 0) {
+ return matches;
+ }
+
+ const compactPrompt = prompt.replace(/[^\w\s/-]+/g, ' ');
+ const segments = compactPrompt
+ .split(/,| with | and | then /i)
+ .map((segment) => segment.trim())
+ .filter((segment) => segment.length >= 3)
+ .slice(0, 6);
+
+ return segments.map((segment) => ({ query: segment }));
+}
+
+export async function groundFlowpilotAssets(prompt: string): Promise {
+ const queries = inferQueriesFromPrompt(prompt);
+ const results = new Map();
+
+ for (const { query, categories } of queries) {
+ const scopes = categories ?? ALL_GROUNDING_CATEGORIES;
+ for (const category of scopes) {
+ const suggestions = await loadDomainAssetSuggestions(category, {
+ query,
+ limit: 4,
+ });
+
+ for (const item of suggestions) {
+ const candidate = toGroundingMatch(item, query);
+ const existing = results.get(candidate.id);
+ if (!existing || existing.confidence < candidate.confidence) {
+ results.set(candidate.id, candidate);
+ }
+ }
+ }
+ }
+
+ return [...results.values()]
+ .sort((left, right) => right.confidence - left.confidence)
+ .slice(0, 8);
+}
+
+export function summarizeAssetGrounding(matches: AssetGroundingMatch[]): string {
+ if (matches.length === 0) {
+ return 'No strong local asset matches were found.';
+ }
+
+ return matches
+ .slice(0, 5)
+ .map((match) => {
+ const suffix = match.archIconPackId ? ` (${match.archIconPackId})` : '';
+ return `${match.label}${suffix}`;
+ })
+ .join(', ');
+}
diff --git a/src/services/flowpilot/prompting.ts b/src/services/flowpilot/prompting.ts
new file mode 100644
index 00000000..9d6b15b5
--- /dev/null
+++ b/src/services/flowpilot/prompting.ts
@@ -0,0 +1,75 @@
+import type { AssetGroundingMatch, FlowpilotPolicyContext } from './types';
+
+export function buildFlowpilotAssistantSystemInstruction(mode: 'answer' | 'plan'): string {
+ if (mode === 'plan') {
+ return [
+ 'You are Flowpilot, a diagramming copilot inside OpenFlowKit.',
+ 'Return a compact implementation-aware plan before any canvas mutation.',
+ 'Do not emit OpenFlow DSL.',
+ 'Be concrete, concise, and action-oriented.',
+ ].join('\n');
+ }
+
+ return [
+ 'You are Flowpilot, a diagramming copilot inside OpenFlowKit.',
+ 'Answer the user directly and practically.',
+ 'Do not emit OpenFlow DSL unless explicitly asked to generate a diagram.',
+ 'Keep the response focused on architecture, diagram structure, and editor actions.',
+ ].join('\n');
+}
+
+export function buildFlowpilotConversationPrompt(
+ prompt: string,
+ context: FlowpilotPolicyContext,
+ assetMatches: AssetGroundingMatch[],
+ mode: 'answer' | 'plan'
+): string {
+ const assetSummary =
+ assetMatches.length > 0
+ ? assetMatches
+ .slice(0, 5)
+ .map((match) => `${match.label} (${match.category})`)
+ .join(', ')
+ : 'none';
+
+ const contextLines = [
+ `Canvas node count: ${context.nodeCount}`,
+ `Selected node count: ${context.selectedNodeCount}`,
+ `Attached image: ${context.hasImage ? 'yes' : 'no'}`,
+ `Grounded local assets: ${assetSummary}`,
+ ];
+
+ const taskLine =
+ mode === 'plan'
+ ? 'Return a short plan with likely next steps and note whether a diagram should be generated next.'
+ : 'Return a direct helpful answer. If a diagram might be useful later, say so briefly without generating it.';
+
+ return [`USER REQUEST: ${prompt}`, '', taskLine, '', 'EDITOR CONTEXT:', ...contextLines].join('\n');
+}
+
+export function buildFlowpilotDiagramPrompt(
+ prompt: string,
+ assetMatches: AssetGroundingMatch[]
+): string {
+ if (assetMatches.length === 0) {
+ return prompt;
+ }
+
+ const assetHints = assetMatches
+ .slice(0, 6)
+ .map((match) => {
+ const packHint = match.archIconPackId ? `, archIconPackId: "${match.archIconPackId}"` : '';
+ const shapeHint = match.archIconShapeId ? `, archIconShapeId: "${match.archIconShapeId}"` : '';
+ return `- ${match.label} -> archProvider: "${match.archProvider || match.category}", archResourceType: "${match.archResourceType || 'service'}"${packHint}${shapeHint}`;
+ })
+ .join('\n');
+
+ return [
+ prompt,
+ '',
+ 'LOCAL ASSET GROUNDING:',
+ assetHints,
+ '',
+ 'Prefer these grounded assets when they match the user request instead of generic icons.',
+ ].join('\n');
+}
diff --git a/src/services/flowpilot/responsePolicy.test.ts b/src/services/flowpilot/responsePolicy.test.ts
new file mode 100644
index 00000000..66c62168
--- /dev/null
+++ b/src/services/flowpilot/responsePolicy.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from 'vitest';
+import { buildFlowpilotPlan, chooseFlowpilotResponseMode } from './responsePolicy';
+
+describe('flowpilot response policy', () => {
+ it('selects answer mode for explanatory prompts', () => {
+ const result = chooseFlowpilotResponseMode({
+ prompt: "Explain what's wrong with this architecture",
+ nodeCount: 8,
+ selectedNodeCount: 0,
+ });
+
+ expect(result.mode).toBe('answer');
+ expect(result.skillId).toBe('explain_existing_diagram');
+ });
+
+ it('selects plan mode for planning prompts', () => {
+ const plan = buildFlowpilotPlan({
+ prompt: 'Plan a better version before drawing it',
+ nodeCount: 5,
+ selectedNodeCount: 0,
+ });
+
+ expect(plan.mode).toBe('plan');
+ expect(plan.steps.length).toBeGreaterThan(1);
+ });
+
+ it('selects preview mode for explicit architecture creation', () => {
+ const result = chooseFlowpilotResponseMode({
+ prompt: 'Create an AWS architecture diagram for a payments platform',
+ nodeCount: 0,
+ selectedNodeCount: 0,
+ });
+
+ expect(result.mode).toBe('diagram_preview');
+ expect(result.requiresApproval).toBe(true);
+ });
+});
diff --git a/src/services/flowpilot/responsePolicy.ts b/src/services/flowpilot/responsePolicy.ts
new file mode 100644
index 00000000..6c21a6e4
--- /dev/null
+++ b/src/services/flowpilot/responsePolicy.ts
@@ -0,0 +1,158 @@
+import { inferPlanSteps } from './skills';
+import type { AgentPlan, AgentResponseMode, FlowpilotPolicyContext, FlowpilotSkillId } from './types';
+
+const EXPLANATION_PATTERNS = [
+ /\bexplain\b/i,
+ /\bwhat('?s| is) wrong\b/i,
+ /\bwhy\b/i,
+ /\breview\b/i,
+ /\banalyze\b/i,
+ /\bcompare\b/i,
+];
+
+const PLANNING_PATTERNS = [
+ /\bplan\b/i,
+ /\bstrategy\b/i,
+ /\boptions\b/i,
+ /\bbefore drawing\b/i,
+ /\boutline\b/i,
+];
+
+const ASSET_PATTERNS = [
+ /\bicon\b/i,
+ /\basset\b/i,
+ /\bcomponent\b/i,
+ /\bwhich service\b/i,
+ /\bwhat should i use\b/i,
+];
+
+const DIAGRAM_PATTERNS = [
+ /\bdiagram\b/i,
+ /\bdraw\b/i,
+ /\bgenerate\b/i,
+ /\bcreate\b/i,
+ /\bshow\b/i,
+ /\bmap\b/i,
+];
+
+const EDIT_PATTERNS = [/\bchange\b/i, /\bupdate\b/i, /\bedit\b/i, /\brefine\b/i, /\breplace\b/i];
+
+const ARCHITECTURE_PATTERNS = [
+ /\barchitecture\b/i,
+ /\baws\b/i,
+ /\bazure\b/i,
+ /\bgcp\b/i,
+ /\bkubernetes\b/i,
+ /\bcncf\b/i,
+ /\binfra\b/i,
+ /\bservice\b/i,
+];
+
+function matchesAny(prompt: string, patterns: RegExp[]): boolean {
+ return patterns.some((pattern) => pattern.test(prompt));
+}
+
+function clampConfidence(value: number): number {
+ return Math.max(0.1, Math.min(0.98, Math.round(value * 100) / 100));
+}
+
+export function chooseFlowpilotResponseMode(
+ context: FlowpilotPolicyContext
+): {
+ mode: AgentResponseMode;
+ confidence: number;
+ requiresApproval: boolean;
+ reasoningSummary: string;
+ skillId: FlowpilotSkillId;
+} {
+ const normalizedPrompt = context.prompt.trim();
+ const hasExplanationIntent = matchesAny(normalizedPrompt, EXPLANATION_PATTERNS);
+ const hasPlanningIntent = matchesAny(normalizedPrompt, PLANNING_PATTERNS);
+ const hasAssetIntent = matchesAny(normalizedPrompt, ASSET_PATTERNS);
+ const hasDiagramIntent = matchesAny(normalizedPrompt, DIAGRAM_PATTERNS);
+ const hasEditIntent = matchesAny(normalizedPrompt, EDIT_PATTERNS);
+ const hasArchitectureIntent = matchesAny(normalizedPrompt, ARCHITECTURE_PATTERNS);
+
+ if (hasAssetIntent && !hasDiagramIntent) {
+ return {
+ mode: 'asset_suggestions',
+ confidence: 0.9,
+ requiresApproval: false,
+ reasoningSummary: 'The request is asking for asset or component guidance before changing the canvas.',
+ skillId: 'suggest_assets',
+ };
+ }
+
+ if (hasExplanationIntent && context.nodeCount > 0) {
+ return {
+ mode: 'answer',
+ confidence: 0.87,
+ requiresApproval: false,
+ reasoningSummary: 'The request is asking for analysis of the current diagram rather than a new draft.',
+ skillId: 'explain_existing_diagram',
+ };
+ }
+
+ if (hasPlanningIntent || (!hasDiagramIntent && normalizedPrompt.split(/\s+/).length < 10)) {
+ return {
+ mode: 'plan',
+ confidence: clampConfidence(hasPlanningIntent ? 0.88 : 0.66),
+ requiresApproval: false,
+ reasoningSummary: 'The request is underspecified or explicitly asks for a plan before drafting.',
+ skillId: 'plan_diagram',
+ };
+ }
+
+ if (context.selectedNodeCount > 0 && hasEditIntent) {
+ return {
+ mode: 'diagram_preview',
+ confidence: 0.9,
+ requiresApproval: true,
+ reasoningSummary: 'The request is a scoped edit on selected nodes, so a preview is safer than applying directly.',
+ skillId: 'edit_selected_nodes',
+ };
+ }
+
+ if (hasArchitectureIntent || hasDiagramIntent) {
+ return {
+ mode: 'diagram_preview',
+ confidence: clampConfidence(hasArchitectureIntent ? 0.92 : 0.84),
+ requiresApproval: true,
+ reasoningSummary: hasArchitectureIntent
+ ? 'The request is architecture-oriented and should ground itself in provider assets before drafting.'
+ : 'The request clearly asks for a diagram draft, so a preview is the right next step.',
+ skillId: hasArchitectureIntent ? 'create_architecture' : 'plan_diagram',
+ };
+ }
+
+ return {
+ mode: 'answer',
+ confidence: 0.62,
+ requiresApproval: false,
+ reasoningSummary: 'The safest next step is to answer in chat first instead of changing the canvas.',
+ skillId: 'answer_question',
+ };
+}
+
+export function buildFlowpilotPlan(context: FlowpilotPolicyContext): AgentPlan {
+ const policy = chooseFlowpilotResponseMode(context);
+ const intendedOutput =
+ policy.mode === 'diagram_preview'
+ ? 'Canvas preview with review/apply controls'
+ : policy.mode === 'asset_suggestions'
+ ? 'Recommended local asset matches'
+ : policy.mode === 'plan'
+ ? 'Structured plan and next-step options'
+ : 'Chat response';
+
+ return {
+ goal: context.prompt.trim(),
+ mode: policy.mode,
+ steps: inferPlanSteps(policy.skillId, context),
+ requiresApproval: policy.requiresApproval,
+ intendedOutput,
+ confidence: policy.confidence,
+ reasoningSummary: policy.reasoningSummary,
+ skillId: policy.skillId,
+ };
+}
diff --git a/src/services/flowpilot/skills.ts b/src/services/flowpilot/skills.ts
new file mode 100644
index 00000000..4180e8f6
--- /dev/null
+++ b/src/services/flowpilot/skills.ts
@@ -0,0 +1,101 @@
+import type {
+ AgentPlan,
+ FlowpilotPolicyContext,
+ FlowpilotSkillDefinition,
+ FlowpilotSkillId,
+} from './types';
+
+export const FLOWPILOT_SKILLS: Record = {
+ answer_question: {
+ id: 'answer_question',
+ label: 'Answer question',
+ outputMode: 'answer',
+ mutatesCanvas: false,
+ requiredContexts: ['canvas', 'chat_history'],
+ fallbackBehavior: 'return_answer',
+ },
+ plan_diagram: {
+ id: 'plan_diagram',
+ label: 'Plan diagram',
+ outputMode: 'plan',
+ mutatesCanvas: false,
+ requiredContexts: ['canvas', 'chat_history'],
+ fallbackBehavior: 'return_plan',
+ },
+ create_architecture: {
+ id: 'create_architecture',
+ label: 'Create architecture',
+ outputMode: 'diagram_preview',
+ mutatesCanvas: true,
+ requiredContexts: ['canvas', 'assets', 'chat_history'],
+ fallbackBehavior: 'return_plan',
+ },
+ edit_selected_nodes: {
+ id: 'edit_selected_nodes',
+ label: 'Edit selected nodes',
+ outputMode: 'diagram_preview',
+ mutatesCanvas: true,
+ requiredContexts: ['canvas', 'selection', 'chat_history'],
+ fallbackBehavior: 'ask_clarifying_question',
+ },
+ upgrade_codebase_import: {
+ id: 'upgrade_codebase_import',
+ label: 'Upgrade codebase import',
+ outputMode: 'diagram_preview',
+ mutatesCanvas: true,
+ requiredContexts: ['canvas', 'assets', 'chat_history'],
+ fallbackBehavior: 'return_plan',
+ },
+ suggest_assets: {
+ id: 'suggest_assets',
+ label: 'Suggest assets',
+ outputMode: 'asset_suggestions',
+ mutatesCanvas: false,
+ requiredContexts: ['assets', 'canvas'],
+ fallbackBehavior: 'return_answer',
+ },
+ explain_existing_diagram: {
+ id: 'explain_existing_diagram',
+ label: 'Explain existing diagram',
+ outputMode: 'answer',
+ mutatesCanvas: false,
+ requiredContexts: ['canvas', 'chat_history'],
+ fallbackBehavior: 'return_answer',
+ },
+};
+
+export function getFlowpilotSkillDefinition(skillId: FlowpilotSkillId): FlowpilotSkillDefinition {
+ return FLOWPILOT_SKILLS[skillId];
+}
+
+export function inferPlanSteps(
+ skillId: FlowpilotSkillId,
+ context: Pick
+): string[] {
+ switch (skillId) {
+ case 'answer_question':
+ return ['Inspect the current canvas context', 'Draft a concise answer'];
+ case 'plan_diagram':
+ return ['Inspect the current canvas context', 'Outline the recommended structure'];
+ case 'create_architecture':
+ return ['Inspect canvas context', 'Look for provider-backed assets', 'Draft a diagram preview'];
+ case 'edit_selected_nodes':
+ return [
+ `Inspect the ${context.selectedNodeCount || 1} selected node${context.selectedNodeCount === 1 ? '' : 's'}`,
+ 'Plan the scoped edit',
+ 'Prepare a diagram preview',
+ ];
+ case 'upgrade_codebase_import':
+ return ['Inspect imported structure', 'Ground platform services', 'Prepare an upgraded preview'];
+ case 'suggest_assets':
+ return ['Search the local asset libraries', 'Rank the best matches'];
+ case 'explain_existing_diagram':
+ return ['Inspect the current diagram', 'Explain strengths, risks, and missing pieces'];
+ default:
+ return ['Inspect the current request', 'Draft the response'];
+ }
+}
+
+export function getFlowpilotSkillLabel(plan: AgentPlan): string {
+ return FLOWPILOT_SKILLS[plan.skillId].label;
+}
diff --git a/src/services/flowpilot/thread.ts b/src/services/flowpilot/thread.ts
new file mode 100644
index 00000000..1e307407
--- /dev/null
+++ b/src/services/flowpilot/thread.ts
@@ -0,0 +1,98 @@
+import { createId } from '@/lib/id';
+import type { ChatMessage } from '@/services/aiService';
+import type {
+ AgentPlan,
+ AssistantThreadItem,
+ AssistantThreadItemType,
+ AssetGroundingMatch,
+} from './types';
+
+function nowIso(): string {
+ return new Date().toISOString();
+}
+
+export function createAssistantThreadItem(
+ type: AssistantThreadItemType,
+ role: AssistantThreadItem['role'],
+ content: string,
+ extra: Partial> = {}
+): AssistantThreadItem {
+ return {
+ id: createId(`flowpilot-${type}`),
+ type,
+ role,
+ content,
+ createdAt: nowIso(),
+ ...extra,
+ };
+}
+
+export function createUserThreadItem(prompt: string, imageBase64?: string): AssistantThreadItem {
+ return createAssistantThreadItem(
+ 'user_message',
+ 'user',
+ imageBase64 ? `${prompt} [Image Attached]` : prompt
+ );
+}
+
+export function createPlanThreadItem(plan: AgentPlan): AssistantThreadItem {
+ return createAssistantThreadItem('assistant_plan', 'model', plan.reasoningSummary, {
+ responseMode: plan.mode,
+ thinkingState: 'planning',
+ summary: plan.reasoningSummary,
+ plan,
+ });
+}
+
+export function createAnswerThreadItem(
+ content: string,
+ mode: AssistantThreadItem['responseMode'],
+ assetMatches?: AssetGroundingMatch[]
+): AssistantThreadItem {
+ const type = mode === 'asset_suggestions' ? 'assistant_recommendation' : 'assistant_lookup_result';
+ return createAssistantThreadItem(type, 'model', content, {
+ responseMode: mode,
+ thinkingState: 'ready',
+ assetMatches,
+ });
+}
+
+export function createPreviewThreadItem(
+ content: string,
+ previewTitle: string,
+ previewDetail?: string,
+ previewStats?: string[],
+ assetMatches?: AssetGroundingMatch[]
+): AssistantThreadItem {
+ return createAssistantThreadItem('assistant_canvas_preview', 'model', content, {
+ responseMode: 'diagram_preview',
+ thinkingState: 'ready',
+ previewTitle,
+ previewDetail,
+ previewStats,
+ assetMatches,
+ });
+}
+
+export function createAppliedThreadItem(summary: string): AssistantThreadItem {
+ return createAssistantThreadItem('assistant_applied_result', 'model', summary, {
+ responseMode: 'diagram_apply_ready',
+ thinkingState: 'ready',
+ applied: true,
+ });
+}
+
+export function createErrorThreadItem(message: string): AssistantThreadItem {
+ return createAssistantThreadItem('assistant_error', 'model', message, {
+ thinkingState: 'error',
+ });
+}
+
+export function assistantThreadToChatMessages(items: AssistantThreadItem[]): ChatMessage[] {
+ return items
+ .filter((item) => item.type !== 'assistant_plan' && item.type !== 'assistant_thinking')
+ .map((item) => ({
+ role: item.role,
+ parts: [{ text: item.content }],
+ }));
+}
diff --git a/src/services/flowpilot/types.ts b/src/services/flowpilot/types.ts
new file mode 100644
index 00000000..9583d39c
--- /dev/null
+++ b/src/services/flowpilot/types.ts
@@ -0,0 +1,95 @@
+import type { DomainLibraryCategory } from '@/services/domainLibrary';
+
+export type AgentResponseMode =
+ | 'answer'
+ | 'plan'
+ | 'asset_suggestions'
+ | 'diagram_preview'
+ | 'diagram_apply_ready'
+ | 'clarification';
+
+export type AgentThinkingState =
+ | 'understanding'
+ | 'gathering_context'
+ | 'searching_assets'
+ | 'planning'
+ | 'generating'
+ | 'ready'
+ | 'error';
+
+export type AssistantThreadItemType =
+ | 'user_message'
+ | 'assistant_thinking'
+ | 'assistant_plan'
+ | 'assistant_lookup_result'
+ | 'assistant_recommendation'
+ | 'assistant_canvas_preview'
+ | 'assistant_applied_result'
+ | 'assistant_error';
+
+export interface AssetGroundingMatch {
+ id: string;
+ label: string;
+ description: string;
+ category: DomainLibraryCategory;
+ archProvider?: string;
+ archResourceType?: string;
+ archIconPackId?: string;
+ archIconShapeId?: string;
+ providerShapeCategory?: string;
+ confidence: number;
+ reasoning: string;
+}
+
+export interface AgentPlan {
+ goal: string;
+ mode: AgentResponseMode;
+ steps: string[];
+ requiresApproval: boolean;
+ intendedOutput: string;
+ confidence: number;
+ reasoningSummary: string;
+ skillId: FlowpilotSkillId;
+}
+
+export type FlowpilotSkillId =
+ | 'answer_question'
+ | 'plan_diagram'
+ | 'create_architecture'
+ | 'edit_selected_nodes'
+ | 'upgrade_codebase_import'
+ | 'suggest_assets'
+ | 'explain_existing_diagram';
+
+export interface FlowpilotSkillDefinition {
+ id: FlowpilotSkillId;
+ label: string;
+ outputMode: AgentResponseMode;
+ mutatesCanvas: boolean;
+ requiredContexts: Array<'canvas' | 'selection' | 'assets' | 'chat_history'>;
+ fallbackBehavior: 'ask_clarifying_question' | 'return_plan' | 'return_answer';
+}
+
+export interface AssistantThreadItem {
+ id: string;
+ role: 'user' | 'model';
+ type: AssistantThreadItemType;
+ content: string;
+ createdAt: string;
+ responseMode?: AgentResponseMode;
+ thinkingState?: AgentThinkingState;
+ summary?: string;
+ plan?: AgentPlan;
+ assetMatches?: AssetGroundingMatch[];
+ previewTitle?: string;
+ previewDetail?: string;
+ previewStats?: string[];
+ applied?: boolean;
+}
+
+export interface FlowpilotPolicyContext {
+ prompt: string;
+ nodeCount: number;
+ selectedNodeCount: number;
+ hasImage?: boolean;
+}
diff --git a/src/services/geminiService.ts b/src/services/geminiService.ts
index b1548902..306ec946 100644
--- a/src/services/geminiService.ts
+++ b/src/services/geminiService.ts
@@ -137,3 +137,47 @@ ${docsContext}
return response.text;
}
+
+export async function chatWithSystemInstructionGemini(
+ history: ChatMessage[],
+ newMessage: string,
+ systemInstruction: string,
+ userApiKey: string,
+ modelId?: string,
+ onChunk?: (delta: string) => void,
+ signal?: AbortSignal,
+): Promise {
+ const ai = new GoogleGenAI({ apiKey: userApiKey });
+ const newMessageContent = {
+ role: 'user' as const,
+ parts: [{ text: newMessage }],
+ };
+ const contents = [...history, newMessageContent];
+
+ const stream = await ai.models.generateContentStream({
+ model: modelId || GEMINI_DEFAULT_MODEL,
+ contents,
+ config: {
+ systemInstruction,
+ responseMimeType: 'text/plain',
+ },
+ });
+
+ let fullText = '';
+ for await (const chunk of stream) {
+ if (signal?.aborted) {
+ throw new DOMException('Aborted', 'AbortError');
+ }
+ const delta = chunk.text ?? '';
+ if (delta) {
+ fullText += delta;
+ onChunk?.(delta);
+ }
+ }
+
+ if (!fullText) {
+ throw new Error('No response from AI');
+ }
+
+ return fullText;
+}
diff --git a/src/services/geminiSystemInstruction.ts b/src/services/geminiSystemInstruction.ts
index 4a10c281..39df5e56 100644
--- a/src/services/geminiSystemInstruction.ts
+++ b/src/services/geminiSystemInstruction.ts
@@ -61,7 +61,6 @@ Your job:
| \`[browser]\` | Web page / frontend screen |
| \`[mobile]\` | Mobile screen |
| \`[note]\` | Callout / annotation |
-| \`[section]\` | Group label |
---
@@ -101,16 +100,7 @@ For \`[architecture]\` nodes use:
api ..> sla
\`\`\`
-8. **Groups & Sections** — use \`group "Label" { ... }\` to cluster related nodes (e.g., layers, swimlanes, frontend/backend):
- \`\`\`
- group "Frontend" {
- [browser] ui: User Interface { icon: "Monitor", color: "blue" }
- }
- group "Backend" {
- [system] api: REST API { icon: "Server", color: "violet" }
- [system] db: Database { icon: "Database", color: "violet" }
- }
- \`\`\`
+8. **No container nodes** — do not use \`[section]\` nodes or \`group {}\` blocks. Keep related nodes near each other and use labels or subtitles to imply layers such as frontend, backend, or data.
---
@@ -158,7 +148,7 @@ For \`[architecture]\` nodes use:
- \`[browser]\`: web apps, dashboards, admin panels, portals
- \`[mobile]\`: iOS, Android, React Native, Flutter apps
- \`[process]\`: operational steps, jobs, transformations, workflows
- - \`[section]\`: layers, trust boundaries, VPCs, clusters, namespaces, zones
+ - Do not use container or group nodes for layers, trust boundaries, VPCs, clusters, namespaces, or zones
13. Label important edges with what flows across them, especially in architecture diagrams: \`HTTP/REST\`, \`SQL\`, \`gRPC\`, \`events\`, \`cache lookup\`, \`files\`
@@ -167,8 +157,8 @@ For \`[architecture]\` nodes use:
15. Do NOT explain the output. Do NOT add prose. Only output DSL.
15b. **Diagram density** — aim for the right density:
- - Flowcharts: 6–15 nodes is ideal. More than 20 = split into groups.
- - Architecture diagrams: 8–20 nodes, grouped by layer using \`[section]\` or \`group {}\`.
+ - Flowcharts: 6–15 nodes is ideal. More than 20 = simplify the diagram.
+ - Architecture diagrams: 8–20 nodes, with layers implied by labels, subtitles, and placement instead of containers.
- Sequence/journey: 4–10 steps in the happy path.
- If a request is simple, keep the diagram simple. Do not pad with unnecessary detail.
@@ -176,8 +166,8 @@ For \`[architecture]\` nodes use:
- Happy path flows TOP → BOTTOM (TB) or LEFT → RIGHT (LR) in a straight line, with alternatives branching off the sides.
- Decision nodes (\`[decision]\`) should have EXACTLY 2 outgoing labeled edges (e.g. \`->|Yes|\` and \`->|No|\`).
- Avoid more than 3 incoming edges on any single node — use a \`[process]\` aggregator if needed.
- - Group tightly coupled nodes in \`group {}\` blocks to keep them visually close.
- - Use \`[section]\` to name architectural layers (e.g. Frontend, Backend, Data, External).
+ - Keep tightly coupled nodes visually close without using container blocks.
+ - Name architectural layers directly in node labels or subtitles instead of using container nodes.
- Use \`==>\` (thick) for the critical path, \`->\` for normal flow, \`..>\` for async/optional, \`-->\` for soft/secondary.
15d. **Self-describing diagrams** — every diagram should be readable without a legend:
@@ -281,18 +271,13 @@ slack_notify ==> live
flow: Serverless API - AWS
direction: TB
-[section] edge: Edge Layer { color: "slate" }
[architecture] cf: CloudFront { archProvider: "aws", archResourceType: "cdn", archIconPackId: "aws-official-starter-v1", color: "blue" }
-[architecture] apigw: API Gateway { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet" }
-
-[section] compute: Compute Layer { color: "blue" }
+[architecture] apigw: API Gateway { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet", subLabel: "Edge Layer" }
[architecture] auth_fn: Auth Lambda { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet" }
-[architecture] api_fn: API Lambda { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet" }
-
-[section] data: Data Layer { color: "emerald" }
+[architecture] api_fn: API Lambda { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "violet", subLabel: "Compute Layer" }
[architecture] dynamo: DynamoDB { archProvider: "aws", archResourceType: "database", archIconPackId: "aws-official-starter-v1", color: "emerald" }
[architecture] cache: ElastiCache { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "yellow" }
-[architecture] s3: S3 Storage { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "emerald" }
+[architecture] s3: S3 Storage { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "emerald", subLabel: "Data Layer" }
[architecture] cognito: Cognito { archProvider: "aws", archResourceType: "service", archIconPackId: "aws-official-starter-v1", color: "amber" }
cf ->|HTTPS| apigw
diff --git a/src/services/infraSync/dockerComposeParser.test.ts b/src/services/infraSync/dockerComposeParser.test.ts
index 532bf515..67af45b4 100644
--- a/src/services/infraSync/dockerComposeParser.test.ts
+++ b/src/services/infraSync/dockerComposeParser.test.ts
@@ -44,9 +44,9 @@ describe('parseDockerCompose', () => {
expect(edge!.label).toBe('depends_on');
});
- it('parses services with networks and links them to network section nodes', () => {
+ it('parses services with networks and links them to network architecture nodes', () => {
const result = parseDockerCompose(COMPOSE_WITH_NETWORKS);
- const netNodes = result.nodes.filter((n) => n.nodeType === 'section');
+ const netNodes = result.nodes.filter((n) => n.nodeType === 'architecture');
expect(netNodes.length).toBeGreaterThanOrEqual(1);
const frontendNode = result.nodes.find((n) => n.resourceName === 'frontend');
diff --git a/src/services/infraSync/dockerComposeParser.ts b/src/services/infraSync/dockerComposeParser.ts
index b1e17f8d..11cf50e3 100644
--- a/src/services/infraSync/dockerComposeParser.ts
+++ b/src/services/infraSync/dockerComposeParser.ts
@@ -174,13 +174,13 @@ export function parseDockerCompose(input: string): InfraSyncResult {
if (isRecord(networkSection)) {
for (const netName of Object.keys(networkSection)) {
const nodeId = sanitizeId(`net_${netName}`);
- nodes.push({
- id: nodeId,
- label: netName,
- nodeType: 'section',
- provider: 'docker-compose',
- resourceType: 'network',
- resourceName: netName,
+ nodes.push({
+ id: nodeId,
+ label: netName,
+ nodeType: 'architecture',
+ provider: 'docker-compose',
+ resourceType: 'network',
+ resourceName: netName,
});
}
}
@@ -202,7 +202,7 @@ export function parseDockerCompose(input: string): InfraSyncResult {
nodes.push({
id: nodeId,
label: net,
- nodeType: 'section',
+ nodeType: 'architecture',
provider: 'docker-compose',
resourceType: 'network',
resourceName: net,
diff --git a/src/services/infraSync/infraToDsl.test.ts b/src/services/infraSync/infraToDsl.test.ts
index b3f45bd3..40ee5e30 100644
--- a/src/services/infraSync/infraToDsl.test.ts
+++ b/src/services/infraSync/infraToDsl.test.ts
@@ -28,20 +28,18 @@ describe('infraSyncResultToDsl', () => {
expect(dsl).toContain('lb_main -> ec2_web');
});
- it('renders section nodes with indented children', () => {
+ it('renders infrastructure nodes as flat DSL nodes', () => {
const result = makeResult({
nodes: [
- { id: 'vpc_main', label: 'Production VPC', nodeType: 'section', provider: 'aws', resourceType: 'aws_vpc', resourceName: 'main' },
+ { id: 'vpc_main', label: 'Production VPC', nodeType: 'architecture', provider: 'aws', resourceType: 'aws_vpc', resourceName: 'main' },
{ id: 'ec2_web', label: 'Web Server', nodeType: 'system', provider: 'aws', resourceType: 'aws_instance', resourceName: 'web' },
],
edges: [{ from: 'vpc_main', to: 'ec2_web' }],
});
const dsl = infraSyncResultToDsl(result);
- expect(dsl).toContain('[section] vpc_main: Production VPC');
- // child should be indented
- expect(dsl).toMatch(/\s+\[system\] ec2_web:/);
- // section->child edge should NOT be rendered as an explicit edge line
- expect(dsl).not.toContain('vpc_main -> ec2_web');
+ expect(dsl).toContain('[architecture] vpc_main: Production VPC');
+ expect(dsl).toContain('[system] ec2_web: Web Server');
+ expect(dsl).toContain('vpc_main -> ec2_web');
});
it('renders edge labels with |label| syntax', () => {
diff --git a/src/services/infraSync/infraToDsl.ts b/src/services/infraSync/infraToDsl.ts
index 933cf309..f7891fb2 100644
--- a/src/services/infraSync/infraToDsl.ts
+++ b/src/services/infraSync/infraToDsl.ts
@@ -12,66 +12,17 @@ export function infraSyncResultToDsl(result: InfraSyncResult, flowTitle?: string
lines.push('direction: LR');
lines.push('');
- const sectionNodes = result.nodes.filter((n) => n.nodeType === 'section');
- const nonSectionNodes = result.nodes.filter((n) => n.nodeType !== 'section');
-
- // Build a set of children per section: non-section nodes that have an edge FROM a section TO them
- const sectionEdges = new Map>();
- for (const section of sectionNodes) {
- sectionEdges.set(section.id, new Set());
- }
-
- const sectionEdgeSet = new Set();
- for (const edge of result.edges) {
- const fromNode = result.nodes.find((n) => n.id === edge.from);
- const toNode = result.nodes.find((n) => n.id === edge.to);
- if (fromNode?.nodeType === 'section' && toNode) {
- const children = sectionEdges.get(fromNode.id);
- if (children) {
- children.add(toNode.id);
- sectionEdgeSet.add(`${edge.from}->${edge.to}`);
- }
- }
- }
-
- // Render section nodes with their children
- const renderedNodeIds = new Set();
-
- for (const section of sectionNodes) {
- const sectionId = sanitizeNodeId(section.id);
- lines.push(`[section] ${sectionId}: ${section.label}`);
- renderedNodeIds.add(section.id);
-
- const children = sectionEdges.get(section.id) ?? new Set();
- for (const childId of children) {
- const child = result.nodes.find((n) => n.id === childId);
- if (!child) continue;
- const childNodeId = sanitizeNodeId(child.id);
- lines.push(` [${child.nodeType}] ${childNodeId}: ${child.label}`);
- renderedNodeIds.add(child.id);
- }
-
- lines.push('');
- }
-
- // Render remaining non-section nodes that haven't been rendered yet
- const remaining = nonSectionNodes.filter((n) => !renderedNodeIds.has(n.id));
- for (const node of remaining) {
+ for (const node of result.nodes) {
const nodeId = sanitizeNodeId(node.id);
lines.push(`[${node.nodeType}] ${nodeId}: ${node.label}`);
- renderedNodeIds.add(node.id);
}
- if (remaining.length > 0) {
+ if (result.nodes.length > 0) {
lines.push('');
}
- // Render edges, skipping section→child edges (already implied by indentation)
const edgeLines: string[] = [];
for (const edge of result.edges) {
- const edgeKey = `${edge.from}->${edge.to}`;
- if (sectionEdgeSet.has(edgeKey)) continue;
-
const from = sanitizeNodeId(edge.from);
const to = sanitizeNodeId(edge.to);
if (edge.label) {
diff --git a/src/services/infraSync/kubernetesParser.test.ts b/src/services/infraSync/kubernetesParser.test.ts
index eae288b1..263a85f3 100644
--- a/src/services/infraSync/kubernetesParser.test.ts
+++ b/src/services/infraSync/kubernetesParser.test.ts
@@ -75,9 +75,9 @@ describe('parseKubernetesManifests', () => {
expect(result.nodes[0].nodeType).toBe('browser');
});
- it('parses a Namespace into a [section] node', () => {
+ it('parses a Namespace into an architecture node', () => {
const result = parseKubernetesManifests(NAMESPACE);
- expect(result.nodes[0].nodeType).toBe('section');
+ expect(result.nodes[0].nodeType).toBe('architecture');
});
it('skips ConfigMap manifests', () => {
diff --git a/src/services/infraSync/kubernetesParser.ts b/src/services/infraSync/kubernetesParser.ts
index e9240d35..49b6026d 100644
--- a/src/services/infraSync/kubernetesParser.ts
+++ b/src/services/infraSync/kubernetesParser.ts
@@ -24,7 +24,7 @@ function getNodeType(kind: string): string {
case 'IngressClass':
return 'browser';
case 'Namespace':
- return 'section';
+ return 'architecture';
case 'CronJob':
case 'Job':
return 'process';
diff --git a/src/services/infraSync/terraformStateParser.test.ts b/src/services/infraSync/terraformStateParser.test.ts
index 1ec1d3e1..4ea0a7d8 100644
--- a/src/services/infraSync/terraformStateParser.test.ts
+++ b/src/services/infraSync/terraformStateParser.test.ts
@@ -48,9 +48,9 @@ describe('parseTerraformState', () => {
expect(result.nodes[0].label).toContain('app');
});
- it('parses aws_vpc into a [section] node', () => {
+ it('parses aws_vpc into an architecture node', () => {
const result = parseTerraformState(makeTfState([makeResource('aws_vpc', 'main')]));
- expect(result.nodes[0].nodeType).toBe('section');
+ expect(result.nodes[0].nodeType).toBe('architecture');
});
it('creates edge from vpc to instance via vpc_id attribute', () => {
diff --git a/src/services/infraSync/terraformStateParser.ts b/src/services/infraSync/terraformStateParser.ts
index d104127a..b1926619 100644
--- a/src/services/infraSync/terraformStateParser.ts
+++ b/src/services/infraSync/terraformStateParser.ts
@@ -97,7 +97,7 @@ function getNodeType(resourceType: string): string {
case 'aws_vpc':
case 'google_compute_network':
case 'azurerm_virtual_network':
- return 'section';
+ return 'architecture';
case 'aws_lb':
case 'aws_alb':
case 'aws_elb':
@@ -108,7 +108,7 @@ function getNodeType(resourceType: string): string {
case 'aws_eks_cluster':
case 'google_container_cluster':
case 'azurerm_kubernetes_cluster':
- return 'section';
+ return 'architecture';
case 'aws_ecs_service':
case 'aws_eks_node_group':
return 'system';
@@ -145,7 +145,7 @@ function getNodeType(resourceType: string): string {
case 'aws_elasticsearch_domain':
return 'system';
case 'google_compute_instance_group':
- return 'section';
+ return 'architecture';
case 'azurerm_app_service':
case 'azurerm_app_service_plan':
return 'system';
diff --git a/src/services/mermaidParser.test.ts b/src/services/mermaidParser.test.ts
index 4b6a788b..1ed0ebbb 100644
--- a/src/services/mermaidParser.test.ts
+++ b/src/services/mermaidParser.test.ts
@@ -122,6 +122,19 @@ describe('mermaidParser', () => {
expect(result.edges[1].label).toBe('1.5a');
});
+ it('ignores subgraph wrappers instead of creating container nodes', () => {
+ const input = `
+ flowchart TD
+ subgraph Services
+ API[API]
+ end
+ `;
+ const result = parseMermaid(input);
+ const apiNode = result.nodes.find((node) => node.id === 'API');
+ expect(result.nodes).toHaveLength(1);
+ expect(apiNode?.parentId).toBeUndefined();
+ });
+
it('should handle duplicate edges between same pair', () => {
const input = `
flowchart TD
diff --git a/src/services/openFlowDSLExporter.ts b/src/services/openFlowDSLExporter.ts
index 35d7bbad..b76586ec 100644
--- a/src/services/openFlowDSLExporter.ts
+++ b/src/services/openFlowDSLExporter.ts
@@ -10,7 +10,8 @@ const TYPE_TO_DSL: Record = {
end: 'end',
custom: 'system',
annotation: 'note',
- section: 'section',
+ section: 'process',
+ group: 'process',
browser: 'browser',
mobile: 'mobile',
container: 'container',
diff --git a/src/services/storage/localFirstRepository.ts b/src/services/storage/localFirstRepository.ts
index 139cbf2a..49dbf6cb 100644
--- a/src/services/storage/localFirstRepository.ts
+++ b/src/services/storage/localFirstRepository.ts
@@ -1,5 +1,11 @@
import type { FlowTab } from '@/lib/types';
import type { ChatMessage } from '@/services/aiService';
+import type {
+ AgentPlan,
+ AgentResponseMode,
+ AgentThinkingState,
+ AssetGroundingMatch,
+} from '@/services/flowpilot/types';
import {
createFlowTabsFromPersistedDocuments,
createPersistedDocumentFromFlowDocument,
@@ -64,6 +70,16 @@ export interface PersistedChatMessage {
role: ChatMessage['role'];
parts: ChatMessage['parts'];
createdAt: string;
+ threadType?: string;
+ responseMode?: AgentResponseMode;
+ thinkingState?: AgentThinkingState;
+ summary?: string;
+ previewTitle?: string;
+ previewDetail?: string;
+ previewStats?: string[];
+ applied?: boolean;
+ plan?: AgentPlan;
+ assetMatches?: AssetGroundingMatch[];
}
export interface PersistedAISettingsRecord {
diff --git a/src/store/actions/createCanvasActions.ts b/src/store/actions/createCanvasActions.ts
index 5c76f2cc..b1c03a57 100644
--- a/src/store/actions/createCanvasActions.ts
+++ b/src/store/actions/createCanvasActions.ts
@@ -12,28 +12,7 @@ export function createCanvasActions(
return {
onNodesChange: (changes) => {
set((state) => {
- const resizedSectionIds = new Set(
- changes
- .filter(
- (change) =>
- change.type === 'dimensions' ||
- change.type === 'replace'
- )
- .map((change) => change.id)
- );
- const nextNodes = applyFlowNodeChanges(changes, state.nodes).map((node) => {
- if (node.type !== 'section' || !resizedSectionIds.has(node.id)) {
- return node;
- }
-
- return {
- ...node,
- data: {
- ...node.data,
- sectionSizingMode: 'manual' as const,
- },
- };
- }) as FlowNode[];
+ const nextNodes = applyFlowNodeChanges(changes, state.nodes) as FlowNode[];
return {
nodes: nextNodes,
tabs: syncTabNodesEdges(state.tabs, state.activeTabId, nextNodes, state.edges),
diff --git a/src/store/persistence.test.ts b/src/store/persistence.test.ts
index b03014e8..ee7581fa 100644
--- a/src/store/persistence.test.ts
+++ b/src/store/persistence.test.ts
@@ -192,7 +192,7 @@ describe('store persistence helpers', () => {
expect(migrated.activeTabId).toBe('tab-a');
});
- it('upgrades legacy section nodes with frame metadata defaults', () => {
+ it('removes legacy section container nodes during migration', () => {
const migrated = migratePersistedFlowState({
tabs: [
{
@@ -216,11 +216,7 @@ describe('store persistence helpers', () => {
tabs: FlowTab[];
};
- const migratedSection = migrated.tabs[0]?.nodes[0];
- expect(migratedSection?.data.sectionSizingMode).toBe('manual');
- expect(migratedSection?.data.sectionLayoutMode).toBe('freeform');
- expect(migratedSection?.data.sectionLocked).toBe(false);
- expect(migratedSection?.data.sectionHidden).toBe(false);
+ expect(migrated.tabs[0]?.nodes).toEqual([]);
});
it('sanitizes persisted ai settings during migration', () => {
diff --git a/src/store/persistence.ts b/src/store/persistence.ts
index bbc8b8c4..99b2b23d 100644
--- a/src/store/persistence.ts
+++ b/src/store/persistence.ts
@@ -108,15 +108,37 @@ export function sanitizePersistedNode(node: FlowTab['nodes'][number]): FlowTab['
return withSectionDefaults(persistedNode);
}
+function flattenLegacyContainerNodes(nodes: FlowTab['nodes']): FlowTab['nodes'] {
+ const removedContainerIds = new Set(
+ nodes
+ .filter((node) => node.type === 'section' || node.type === 'group')
+ .map((node) => node.id)
+ );
+
+ return nodes
+ .filter((node) => !removedContainerIds.has(node.id))
+ .map((node) => {
+ if (!node.parentId || !removedContainerIds.has(node.parentId)) {
+ return node;
+ }
+
+ const { parentId: _parentId, extent: _extent, ...flattenedNode } = node;
+ return flattenedNode;
+ });
+}
+
export function sanitizePersistedEdge(edge: FlowTab['edges'][number]): FlowTab['edges'][number] {
const { selected: _selected, ...persistedEdge } = edge;
return persistedEdge;
}
export function sanitizePersistedTab(tab: FlowTab): FlowTab {
+ const sanitizedNodes = tab.nodes.map(sanitizePersistedNode);
+ const flattenedNodes = flattenLegacyContainerNodes(sanitizedNodes);
+
return {
...tab,
- nodes: ensureParentsBeforeChildren(tab.nodes.map(sanitizePersistedNode)),
+ nodes: ensureParentsBeforeChildren(flattenedNodes),
edges: tab.edges.map(sanitizePersistedEdge),
playback: clonePlaybackState(tab.playback),
history: sanitizePersistedHistory(tab.history),
diff --git a/src/store/workspaceDocumentModel.ts b/src/store/workspaceDocumentModel.ts
index 17be3fbc..e2ac6a56 100644
--- a/src/store/workspaceDocumentModel.ts
+++ b/src/store/workspaceDocumentModel.ts
@@ -280,7 +280,7 @@ function resolveNodeSize(node: FlowNode): { width: number; height: number } {
}
function isPreviewContainerNode(node: FlowNode): boolean {
- return node.type === 'group' || node.type === 'section' || node.type === 'swimlane';
+ return node.type === 'swimlane';
}
function clamp(value: number, min: number, max: number): number {
diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo
index 9ce3ade7..a440610e 100644
--- a/tsconfig.tsbuildinfo
+++ b/tsconfig.tsbuildinfo
@@ -1 +1 @@
-{"root":["./src/app.test.tsx","./src/app.tsx","./src/constants.test.ts","./src/constants.ts","./src/index.tsx","./src/store.test.ts","./src/store.ts","./src/theme.test.ts","./src/theme.ts","./src/app/routestate.test.ts","./src/app/routestate.ts","./src/components/annotationnode.tsx","./src/components/commandbar.test.tsx","./src/components/commandbar.tsx","./src/components/connectmenu.test.tsx","./src/components/connectmenu.tsx","./src/components/connectmenusections.tsx","./src/components/contextmenu.test.tsx","./src/components/contextmenu.tsx","./src/components/customconnectionline.test.tsx","./src/components/customconnectionline.tsx","./src/components/customedge.tsx","./src/components/customnode.handleinteraction.test.tsx","./src/components/customnode.tsx","./src/components/customnodecontent.tsx","./src/components/designsystem.integration.test.tsx","./src/components/diagramviewer.tsx","./src/components/errorboundary.tsx","./src/components/exportmenu.tsx","./src/components/exportmenupanel.test.tsx","./src/components/exportmenupanel.tsx","./src/components/flowcanvas.tsx","./src/components/floweditor.tsx","./src/components/floweditoremptystate.tsx","./src/components/floweditorlayoutoverlay.tsx","./src/components/floweditorpanels.test.tsx","./src/components/floweditorpanels.tsx","./src/components/flowtabs.test.tsx","./src/components/flowtabs.tsx","./src/components/groupnode.tsx","./src/components/homepage.integration.test.tsx","./src/components/homepage.tsx","./src/components/iconassetnodebody.tsx","./src/components/iconmap.test.tsx","./src/components/iconmap.ts","./src/components/imagenode.tsx","./src/components/importrecoverydialog.test.tsx","./src/components/importrecoverydialog.tsx","./src/components/inlinetexteditsurface.test.tsx","./src/components/inlinetexteditsurface.tsx","./src/components/keyboardshortcutsmodal.tsx","./src/components/languageselector.tsx","./src/components/markdownrenderer.tsx","./src/components/memoizedmarkdown.tsx","./src/components/navigationcontrols.tsx","./src/components/nodebadges.tsx","./src/components/nodechrome.tsx","./src/components/nodequickcreatebuttons.tsx","./src/components/nodeshapesvg.tsx","./src/components/nodetransformcontrols.tsx","./src/components/playbackcontrols.tsx","./src/components/propertiespanel.tsx","./src/components/rightrail.tsx","./src/components/sectionnode.tsx","./src/components/shareembedmodal.tsx","./src/components/sharemodal.test.tsx","./src/components/sharemodal.tsx","./src/components/sidebarshell.tsx","./src/components/snapshotspanel.test.tsx","./src/components/snapshotspanel.tsx","./src/components/studioaipanel.test.tsx","./src/components/studioaipanel.tsx","./src/components/studioaipanelsections.tsx","./src/components/studiocodepanel.test.tsx","./src/components/studiocodepanel.tsx","./src/components/studiopanel.test.tsx","./src/components/studiopanel.tsx","./src/components/studioplaybackpanel.test.tsx","./src/components/studioplaybackpanel.tsx","./src/components/swimlanenode.tsx","./src/components/textnode.tsx","./src/components/themetoggle.tsx","./src/components/toolbar.tsx","./src/components/tooltip.tsx","./src/components/topnav.tsx","./src/components/welcomemodal.tsx","./src/components/annotationtheme.ts","./src/components/container-nodes.handleinteraction.test.tsx","./src/components/editorsurfacetiers.ts","./src/components/handleinteraction.test.ts","./src/components/handleinteraction.ts","./src/components/handleinteractionusage.test.ts","./src/components/lightweight-nodes.handleinteraction.test.tsx","./src/components/markdownsyntax.ts","./src/components/nodehelpers.test.ts","./src/components/nodehelpers.ts","./src/components/sharemodalcontent.ts","./src/components/studioaicopy.ts","./src/components/studioaipanelexamples.ts","./src/components/transformdiagnostics.test.ts","./src/components/transformdiagnostics.ts","./src/components/useactivenodeselection.ts","./src/components/useexportmenu.test.tsx","./src/components/useexportmenu.ts","./src/components/settingsmodal/aisettings.tsx","./src/components/settingsmodal/canvassettings.tsx","./src/components/settingsmodal/generalsettings.test.tsx","./src/components/settingsmodal/generalsettings.tsx","./src/components/settingsmodal/settingsmodal.test.tsx","./src/components/settingsmodal/settingsmodal.tsx","./src/components/settingsmodal/shortcutssettings.tsx","./src/components/settingsmodal/ai/advancedendpointsection.tsx","./src/components/settingsmodal/ai/customheaderseditor.tsx","./src/components/settingsmodal/ai/privacysection.tsx","./src/components/settingsmodal/ai/providericon.tsx","./src/components/settingsmodal/ai/providersection.tsx","./src/components/settingsmodal/ai/step.tsx","./src/components/add-items/additemregistry.tsx","./src/components/app/docssiteredirect.test.tsx","./src/components/app/docssiteredirect.tsx","./src/components/app/mobileworkspacegate.test.tsx","./src/components/app/mobileworkspacegate.tsx","./src/components/app/routeloadingfallback.test.tsx","./src/components/app/routeloadingfallback.tsx","./src/components/app/mobileworkspacegatecopy.ts","./src/components/app/routeloadingcopy.ts","./src/components/architecture-lint/lintrulespanel.tsx","./src/components/architecture-lint/ruleform.tsx","./src/components/architecture-lint/visualeditor.tsx","./src/components/command-bar/assetsview.test.tsx","./src/components/command-bar/assetsview.tsx","./src/components/command-bar/codebaseimportpanels.tsx","./src/components/command-bar/codebaseimportsection.tsx","./src/components/command-bar/designsystemeditor.tsx","./src/components/command-bar/designsystemview.tsx","./src/components/command-bar/diagramminipreview.tsx","./src/components/command-bar/figmaimportpanel.tsx","./src/components/command-bar/importsurfaceprimitives.tsx","./src/components/command-bar/importview.tsx","./src/components/command-bar/importviewpanels.tsx","./src/components/command-bar/layersview.tsx","./src/components/command-bar/layoutview.tsx","./src/components/command-bar/pagesview.tsx","./src/components/command-bar/rootview.test.tsx","./src/components/command-bar/rootview.tsx","./src/components/command-bar/searchview.tsx","./src/components/command-bar/templatesview.test.tsx","./src/components/command-bar/templatesview.tsx","./src/components/command-bar/viewheader.tsx","./src/components/command-bar/visualsview.tsx","./src/components/command-bar/applycodechanges.ts","./src/components/command-bar/assetsviewconstants.ts","./src/components/command-bar/importdetection.test.ts","./src/components/command-bar/importdetection.ts","./src/components/command-bar/importnativeparsers.ts","./src/components/command-bar/importviewmodel.test.ts","./src/components/command-bar/importviewmodel.ts","./src/components/command-bar/mermaidimportparser.ts","./src/components/command-bar/searchquery.test.ts","./src/components/command-bar/searchquery.ts","./src/components/command-bar/types.ts","./src/components/command-bar/useaiviewstate.test.tsx","./src/components/command-bar/useaiviewstate.ts","./src/components/command-bar/usecloudassetcatalog.ts","./src/components/command-bar/usecommandbarcommands.test.tsx","./src/components/command-bar/usecommandbarcommands.tsx","./src/components/custom-edge/customedgewrapper.tsx","./src/components/custom-edge/edgemarkerdefs.tsx","./src/components/custom-edge/sequencemessageedge.test.tsx","./src/components/custom-edge/sequencemessageedge.tsx","./src/components/custom-edge/animatededgepresentation.test.ts","./src/components/custom-edge/animatededgepresentation.ts","./src/components/custom-edge/classrelationsemantics.test.ts","./src/components/custom-edge/classrelationsemantics.ts","./src/components/custom-edge/edgerendermode.ts","./src/components/custom-edge/pathutils.test.ts","./src/components/custom-edge/pathutils.ts","./src/components/custom-edge/pathutilsgeometry.ts","./src/components/custom-edge/pathutilssiblingrouting.ts","./src/components/custom-edge/pathutilstypes.ts","./src/components/custom-edge/relationroutingsemantics.test.ts","./src/components/custom-edge/relationroutingsemantics.ts","./src/components/custom-edge/standardedgemarkers.test.ts","./src/components/custom-edge/standardedgemarkers.ts","./src/components/custom-nodes/architecturenode.handleinteraction.test.tsx","./src/components/custom-nodes/architecturenode.tsx","./src/components/custom-nodes/browsernode.tsx","./src/components/custom-nodes/classentitynode.handleinteraction.test.tsx","./src/components/custom-nodes/classnode.tsx","./src/components/custom-nodes/entitynode.tsx","./src/components/custom-nodes/journeynode.tsx","./src/components/custom-nodes/mindmapnode.tsx","./src/components/custom-nodes/mobilenode.tsx","./src/components/custom-nodes/sequencenotenode.tsx","./src/components/custom-nodes/sequenceparticipantnode.tsx","./src/components/custom-nodes/structurednodehandles.tsx","./src/components/custom-nodes/visualnodes.handleinteraction.test.tsx","./src/components/custom-nodes/browservariantrenderer.tsx","./src/components/custom-nodes/mobilevariantrenderer.tsx","./src/components/custom-nodes/structuredlistnavigation.test.ts","./src/components/custom-nodes/structuredlistnavigation.ts","./src/components/custom-nodes/variantconstants.ts","./src/components/diagram-diff/diffmodebanner.tsx","./src/components/flow-canvas/flowcanvasalignmentguidesoverlay.tsx","./src/components/flow-canvas/flowcanvasoverlays.tsx","./src/components/flow-canvas/flowcanvassurface.test.tsx","./src/components/flow-canvas/flowcanvassurface.tsx","./src/components/flow-canvas/streamingoverlay.tsx","./src/components/flow-canvas/alignmentguides.test.ts","./src/components/flow-canvas/alignmentguides.ts","./src/components/flow-canvas/flowcanvasreactflowcontracts.ts","./src/components/flow-canvas/flowcanvastypes.test.ts","./src/components/flow-canvas/flowcanvastypes.tsx","./src/components/flow-canvas/largegraphsafetymode.test.ts","./src/components/flow-canvas/largegraphsafetymode.ts","./src/components/flow-canvas/nodechromecoverage.test.ts","./src/components/flow-canvas/pastehelpers.test.ts","./src/components/flow-canvas/pastehelpers.ts","./src/components/flow-canvas/useflowcanvasalignmentguides.ts","./src/components/flow-canvas/useflowcanvasconnectionstate.ts","./src/components/flow-canvas/useflowcanvascontextactions.ts","./src/components/flow-canvas/useflowcanvasdragdrop.ts","./src/components/flow-canvas/useflowcanvasinteractionlod.ts","./src/components/flow-canvas/useflowcanvasmenus.ts","./src/components/flow-canvas/useflowcanvasmenusandactions.test.tsx","./src/components/flow-canvas/useflowcanvasmenusandactions.ts","./src/components/flow-canvas/useflowcanvasnodedraghandlers.ts","./src/components/flow-canvas/useflowcanvaspaste.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.test.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.ts","./src/components/flow-canvas/useflowcanvasselectiontools.test.tsx","./src/components/flow-canvas/useflowcanvasselectiontools.ts","./src/components/flow-canvas/useflowcanvasviewstate.test.ts","./src/components/flow-canvas/useflowcanvasviewstate.ts","./src/components/flow-canvas/useflowcanvaszoomlod.ts","./src/components/flow-editor/collaborationpresenceoverlay.test.tsx","./src/components/flow-editor/collaborationpresenceoverlay.tsx","./src/components/flow-editor/floweditorchrome.tsx","./src/components/flow-editor/buildfloweditorcontrollerparams.ts","./src/components/flow-editor/buildfloweditorscreencontrollerparams.ts","./src/components/flow-editor/chromeproptypes.ts","./src/components/flow-editor/floweditorchromeprops.ts","./src/components/flow-editor/infradslapply.test.ts","./src/components/flow-editor/infradslapply.ts","./src/components/flow-editor/panelprops.test.ts","./src/components/flow-editor/panelprops.ts","./src/components/flow-editor/shouldexitstudioonselection.test.ts","./src/components/flow-editor/shouldexitstudioonselection.ts","./src/components/flow-editor/usecollaborationnodepositions.ts","./src/components/flow-editor/usefloweditorcontroller.ts","./src/components/flow-editor/usefloweditorinteractionbindings.ts","./src/components/flow-editor/usefloweditorpanelactions.ts","./src/components/flow-editor/usefloweditorpanelprops.ts","./src/components/flow-editor/usefloweditorruntime.ts","./src/components/flow-editor/usefloweditorscreenbehavior.ts","./src/components/flow-editor/usefloweditorscreenmodel.ts","./src/components/flow-editor/usefloweditorscreenstate.ts","./src/components/flow-editor/usefloweditorshellcontroller.test.tsx","./src/components/flow-editor/usefloweditorshellcontroller.ts","./src/components/flow-editor/usefloweditorstudiocontroller.test.tsx","./src/components/flow-editor/usefloweditorstudiocontroller.ts","./src/components/flow-editor/useinfradslapply.ts","./src/components/home/githubcard.tsx","./src/components/home/homedashboard.tsx","./src/components/home/homeflowdialogs.tsx","./src/components/home/homesettingsview.tsx","./src/components/home/homesidebar.tsx","./src/components/home/hometemplatesview.tsx","./src/components/home/sidebarfooter.tsx","./src/components/home/welcomemodalstate.ts","./src/components/icons/assetsicon.tsx","./src/components/icons/openflowlogo.tsx","./src/components/infra-sync/infrasyncpanel.test.tsx","./src/components/infra-sync/infrasyncpanel.tsx","./src/components/journey/journeyscorecontrol.test.tsx","./src/components/journey/journeyscorecontrol.tsx","./src/components/properties/bulknodeproperties.test.tsx","./src/components/properties/bulknodeproperties.tsx","./src/components/properties/bulknodepropertiesfamilysections.tsx","./src/components/properties/bulknodepropertiessections.tsx","./src/components/properties/bulknodepropertiesutilitysections.tsx","./src/components/properties/colorpicker.test.tsx","./src/components/properties/colorpicker.tsx","./src/components/properties/customcolorpopover.tsx","./src/components/properties/diagramnodepropertiesrouter.test.ts","./src/components/properties/diagramnodepropertiesrouter.tsx","./src/components/properties/edgeproperties.tsx","./src/components/properties/iconpicker.tsx","./src/components/properties/icontilepickerprimitives.tsx","./src/components/properties/imageupload.tsx","./src/components/properties/inspectorprimitives.tsx","./src/components/properties/nodeactionbuttons.tsx","./src/components/properties/nodecontentsection.tsx","./src/components/properties/nodeimagesettingssection.tsx","./src/components/properties/nodeproperties.test.tsx","./src/components/properties/nodeproperties.tsx","./src/components/properties/nodewireframevariantsection.tsx","./src/components/properties/propertysliderrow.tsx","./src/components/properties/segmentedchoice.tsx","./src/components/properties/shapeselector.tsx","./src/components/properties/swatchpicker.tsx","./src/components/properties/bulknodepropertiesmodel.test.ts","./src/components/properties/bulknodepropertiesmodel.ts","./src/components/properties/colorpickerutils.ts","./src/components/properties/propertyinputbehavior.test.ts","./src/components/properties/propertyinputbehavior.ts","./src/components/properties/sectionactionbuilder.ts","./src/components/properties/shared.ts","./src/components/properties/wireframevariants.ts","./src/components/properties/edge/architectureedgesemanticssection.tsx","./src/components/properties/edge/edgecolorsection.test.tsx","./src/components/properties/edge/edgecolorsection.tsx","./src/components/properties/edge/edgeconditionsection.tsx","./src/components/properties/edge/edgelabelsection.tsx","./src/components/properties/edge/edgerelationsection.test.tsx","./src/components/properties/edge/edgerelationsection.tsx","./src/components/properties/edge/edgeroutesection.test.tsx","./src/components/properties/edge/edgeroutesection.tsx","./src/components/properties/edge/edgestylesection.tsx","./src/components/properties/edge/sequencemessagesection.tsx","./src/components/properties/edge/architecturesemantics.test.ts","./src/components/properties/edge/architecturesemantics.ts","./src/components/properties/edge/edgecolorutils.ts","./src/components/properties/edge/edgelabelmodel.test.ts","./src/components/properties/edge/edgelabelmodel.ts","./src/components/properties/edge/errelationoptions.ts","./src/components/properties/families/architecturenodeproperties.test.tsx","./src/components/properties/families/architecturenodeproperties.tsx","./src/components/properties/families/architecturenodesection.tsx","./src/components/properties/families/classdiagramnodeproperties.tsx","./src/components/properties/families/classmemberlisteditor.tsx","./src/components/properties/families/classnodesection.tsx","./src/components/properties/families/erdiagramnodeproperties.tsx","./src/components/properties/families/entityfieldlisteditor.tsx","./src/components/properties/families/entitynodesection.tsx","./src/components/properties/families/journeynodeproperties.tsx","./src/components/properties/families/journeynodesection.tsx","./src/components/properties/families/mindmapnodeproperties.test.tsx","./src/components/properties/families/mindmapnodeproperties.tsx","./src/components/properties/families/sequencenodeproperties.tsx","./src/components/properties/families/sequencenodesection.tsx","./src/components/properties/families/specializednodecolorsections.test.tsx","./src/components/properties/families/structuredtextlisteditor.tsx","./src/components/properties/families/architectureoptions.ts","./src/components/studio-code-panel/usestudiocodepanelcontroller.test.tsx","./src/components/studio-code-panel/usestudiocodepanelcontroller.ts","./src/components/templates/templatepresentation.tsx","./src/components/toolbar/toolbaraddmenu.test.tsx","./src/components/toolbar/toolbaraddmenu.tsx","./src/components/toolbar/toolbaraddmenupanel.tsx","./src/components/toolbar/toolbarhistorycontrols.tsx","./src/components/toolbar/toolbarmodecontrols.tsx","./src/components/toolbar/toolbarbuttonstyles.ts","./src/components/top-nav/savestatusindicator.tsx","./src/components/top-nav/topnavactions.tsx","./src/components/top-nav/topnavbrand.tsx","./src/components/top-nav/topnavmenu.test.tsx","./src/components/top-nav/topnavmenu.tsx","./src/components/top-nav/topnavmenupanel.tsx","./src/components/top-nav/usetopnavstate.ts","./src/components/ui/button.tsx","./src/components/ui/collapsiblesection.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/searchfield.tsx","./src/components/ui/segmentedtabs.tsx","./src/components/ui/select.test.tsx","./src/components/ui/select.tsx","./src/components/ui/sidebaritem.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toastcontext.tsx","./src/components/ui/editorfieldstyles.ts","./src/config/aiproviders.test.ts","./src/config/aiproviders.ts","./src/config/rolloutflags.ts","./src/context/architecturelintcontext.tsx","./src/context/cinematicexportcontext.tsx","./src/context/diagramdiffcontext.tsx","./src/context/themecontext.tsx","./src/diagram-types/bootstrap.test.ts","./src/diagram-types/bootstrap.ts","./src/diagram-types/builtinplugins.ts","./src/diagram-types/builtinpropertypanels.ts","./src/diagram-types/registerbuiltinplugins.test.ts","./src/diagram-types/registerbuiltinplugins.ts","./src/diagram-types/registerbuiltinpropertypanels.test.ts","./src/diagram-types/registerbuiltinpropertypanels.ts","./src/diagram-types/architecture/fuzzcorpus.test.ts","./src/diagram-types/architecture/plugin.test.ts","./src/diagram-types/architecture/plugin.ts","./src/diagram-types/classdiagram/fuzzcorpus.test.ts","./src/diagram-types/classdiagram/plugin.test.ts","./src/diagram-types/classdiagram/plugin.ts","./src/diagram-types/core/contracts.ts","./src/diagram-types/core/index.ts","./src/diagram-types/core/propertypanels.test.ts","./src/diagram-types/core/propertypanels.ts","./src/diagram-types/core/registry.ts","./src/diagram-types/erdiagram/fuzzcorpus.test.ts","./src/diagram-types/erdiagram/plugin.test.ts","./src/diagram-types/erdiagram/plugin.ts","./src/diagram-types/flowchart/plugin.ts","./src/diagram-types/journey/fuzzcorpus.test.ts","./src/diagram-types/journey/plugin.test.ts","./src/diagram-types/journey/plugin.ts","./src/diagram-types/mindmap/fuzzcorpus.test.ts","./src/diagram-types/mindmap/plugin.test.ts","./src/diagram-types/mindmap/plugin.ts","./src/diagram-types/sequence/plugin.test.ts","./src/diagram-types/sequence/plugin.ts","./src/diagram-types/statediagram/plugin.test.ts","./src/diagram-types/statediagram/plugin.ts","./src/docs/docsroutes.ts","./src/docs/publicdocscatalog.js","./src/hooks/edgeconnectinteractions.test.ts","./src/hooks/edgeconnectinteractions.ts","./src/hooks/flowexportviewport.test.ts","./src/hooks/flowexportviewport.ts","./src/hooks/mindmaptopicactionrequest.ts","./src/hooks/nodelabeleditrequest.test.ts","./src/hooks/nodelabeleditrequest.ts","./src/hooks/nodequickcreaterequest.test.ts","./src/hooks/nodequickcreaterequest.ts","./src/hooks/snapshotpolicy.test.ts","./src/hooks/snapshotpolicy.ts","./src/hooks/useaigeneration.ts","./src/hooks/useanalyticspreference.ts","./src/hooks/useanimatededgeperformancewarning.test.ts","./src/hooks/useanimatededgeperformancewarning.ts","./src/hooks/useassetcatalog.ts","./src/hooks/usecinematicexport.ts","./src/hooks/useclipboardoperations.ts","./src/hooks/usedesignsystem.ts","./src/hooks/useedgeinteractions.ts","./src/hooks/useedgeoperations.ts","./src/hooks/usefloweditoractions.test.ts","./src/hooks/usefloweditoractions.ts","./src/hooks/usefloweditorcallbacks.ts","./src/hooks/usefloweditorcollaboration.test.ts","./src/hooks/usefloweditorcollaboration.ts","./src/hooks/usefloweditoruistate.ts","./src/hooks/useflowexport.ts","./src/hooks/useflowhistory.test.ts","./src/hooks/useflowhistory.ts","./src/hooks/useflowoperations.ts","./src/hooks/usegithubstars.ts","./src/hooks/useinfrasync.ts","./src/hooks/useinlinenodetextedit.test.ts","./src/hooks/useinlinenodetextedit.ts","./src/hooks/usekeyboardshortcuts.test.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/uselayoutoperations.ts","./src/hooks/usemarkdowneditor.ts","./src/hooks/usemenukeyboardnavigation.ts","./src/hooks/usemodifierkeys.test.ts","./src/hooks/usemodifierkeys.ts","./src/hooks/usenodeoperations.ts","./src/hooks/useplayback.ts","./src/hooks/useprovidershapepreview.ts","./src/hooks/userecentimports.ts","./src/hooks/useshiftheld.ts","./src/hooks/usesnapshots.ts","./src/hooks/usestaticexport.ts","./src/hooks/usestoragepressureguard.ts","./src/hooks/usestructuredlisteditor.ts","./src/hooks/usestyleclipboard.test.ts","./src/hooks/usestyleclipboard.ts","./src/hooks/ai-generation/chathistorystorage.test.ts","./src/hooks/ai-generation/chathistorystorage.ts","./src/hooks/ai-generation/codetoarchitecture.test.ts","./src/hooks/ai-generation/codetoarchitecture.ts","./src/hooks/ai-generation/codebaseanalyzer.test.ts","./src/hooks/ai-generation/codebaseanalyzer.ts","./src/hooks/ai-generation/codebaseparser.test.ts","./src/hooks/ai-generation/codebaseparser.ts","./src/hooks/ai-generation/codebasetonativediagram.test.ts","./src/hooks/ai-generation/codebasetonativediagram.ts","./src/hooks/ai-generation/graphcomposer.test.ts","./src/hooks/ai-generation/graphcomposer.ts","./src/hooks/ai-generation/nodeactionprompts.test.ts","./src/hooks/ai-generation/nodeactionprompts.ts","./src/hooks/ai-generation/openapiparser.test.ts","./src/hooks/ai-generation/openapiparser.ts","./src/hooks/ai-generation/openapitosequence.ts","./src/hooks/ai-generation/positionpreservingapply.test.ts","./src/hooks/ai-generation/positionpreservingapply.ts","./src/hooks/ai-generation/readiness.test.ts","./src/hooks/ai-generation/readiness.ts","./src/hooks/ai-generation/requestlifecycle.test.ts","./src/hooks/ai-generation/requestlifecycle.ts","./src/hooks/ai-generation/sqlparser.test.ts","./src/hooks/ai-generation/sqlparser.ts","./src/hooks/ai-generation/sqltoerd.ts","./src/hooks/ai-generation/streamingparser.ts","./src/hooks/ai-generation/streamingstore.ts","./src/hooks/ai-generation/terraformtocloud.ts","./src/hooks/edge-operations/utils.test.ts","./src/hooks/edge-operations/utils.ts","./src/hooks/flow-editor-actions/exporthandlers.test.ts","./src/hooks/flow-editor-actions/exporthandlers.ts","./src/hooks/flow-editor-actions/helpers.ts","./src/hooks/flow-editor-actions/layouthandlers.test.ts","./src/hooks/flow-editor-actions/layouthandlers.ts","./src/hooks/flow-export/diagramdocumenttransfer.test.ts","./src/hooks/flow-export/diagramdocumenttransfer.ts","./src/hooks/flow-export/exportcapture.test.ts","./src/hooks/flow-export/exportcapture.ts","./src/hooks/flow-operations/useflowcoreactions.ts","./src/hooks/node-operations/createconnectedsibling.ts","./src/hooks/node-operations/dragstopreconcilepolicy.test.ts","./src/hooks/node-operations/dragstopreconcilepolicy.ts","./src/hooks/node-operations/nodefactories.ts","./src/hooks/node-operations/routingduringdrag.test.ts","./src/hooks/node-operations/routingduringdrag.ts","./src/hooks/node-operations/sectionbounds.ts","./src/hooks/node-operations/sectionhittesting.ts","./src/hooks/node-operations/sectionoperations.ts","./src/hooks/node-operations/usearchitecturenodeoperations.test.ts","./src/hooks/node-operations/usearchitecturenodeoperations.ts","./src/hooks/node-operations/usemindmapnodeoperations.test.ts","./src/hooks/node-operations/usemindmapnodeoperations.ts","./src/hooks/node-operations/usenodedragoperations.test.ts","./src/hooks/node-operations/usenodedragoperations.ts","./src/hooks/node-operations/usenodeoperationadders.ts","./src/hooks/node-operations/utils.test.ts","./src/hooks/node-operations/utils.ts","./src/i18n/config.test.ts","./src/i18n/config.ts","./src/i18n/strictmodelocalecoverage.test.ts","./src/i18n/usedlocalecoverage.test.ts","./src/lib/architecturetemplatedata.ts","./src/lib/architecturetemplates.test.ts","./src/lib/architecturetemplates.ts","./src/lib/brand.ts","./src/lib/classmembers.ts","./src/lib/colorutils.ts","./src/lib/connectcreationpolicy.test.ts","./src/lib/connectcreationpolicy.ts","./src/lib/date.ts","./src/lib/designtokens.ts","./src/lib/devperformance.ts","./src/lib/entityfields.ts","./src/lib/ertoclassconversion.test.ts","./src/lib/ertoclassconversion.ts","./src/lib/exportfilename.test.ts","./src/lib/exportfilename.ts","./src/lib/flowminddslparserv2.test.ts","./src/lib/flowminddslparserv2.ts","./src/lib/fuzzymatch.ts","./src/lib/genericshapepolicy.ts","./src/lib/id.ts","./src/lib/index.ts","./src/lib/legacybranding.ts","./src/lib/logger.ts","./src/lib/mermaidparser.ts","./src/lib/mermaidparserhelpers.ts","./src/lib/mermaidparsermodel.ts","./src/lib/mindmaplayout.test.ts","./src/lib/mindmaplayout.ts","./src/lib/mindmaplayoutengine.ts","./src/lib/mindmaptree.ts","./src/lib/nodebulkediting.test.ts","./src/lib/nodebulkediting.ts","./src/lib/nodehandles.ts","./src/lib/nodeparent.ts","./src/lib/nodestyledata.test.ts","./src/lib/nodestyledata.ts","./src/lib/openflowdslparser.ts","./src/lib/openflowdslparserv2.ts","./src/lib/reactflowcompat.ts","./src/lib/reconnectedge.test.ts","./src/lib/reconnectedge.ts","./src/lib/relationsemantics.test.ts","./src/lib/relationsemantics.ts","./src/lib/releasestaleelkroutes.test.ts","./src/lib/releasestaleelkroutes.ts","./src/lib/result.ts","./src/lib/storagepressure.test.ts","./src/lib/storagepressure.ts","./src/lib/types.ts","./src/lib/xsssafety.test.tsx","./src/services/aligndistribute.ts","./src/services/aiservice.test.ts","./src/services/aiservice.ts","./src/services/aiserviceschemas.ts","./src/services/animatedexport.test.ts","./src/services/animatedexport.ts","./src/services/architectureroundtrip.test.ts","./src/services/assetcatalog.test.ts","./src/services/assetcatalog.ts","./src/services/assetpresentation.ts","./src/services/canonicalserialization.test.ts","./src/services/canonicalserialization.ts","./src/services/composediagramfordisplay.test.ts","./src/services/composediagramfordisplay.ts","./src/services/diagramdocument.test.ts","./src/services/diagramdocument.ts","./src/services/diagramdocumentschemas.ts","./src/services/domainlibrary.test.ts","./src/services/domainlibrary.ts","./src/services/elklayout.test.ts","./src/services/elklayout.ts","./src/services/exportservice.test.ts","./src/services/exportservice.ts","./src/services/figmaexportservice.ts","./src/services/flowchartroundtrip.test.ts","./src/services/geminiservice.ts","./src/services/geminisysteminstruction.ts","./src/services/gifencoder.test.ts","./src/services/gifencoder.ts","./src/services/githubfetcher.test.ts","./src/services/githubfetcher.ts","./src/services/iconassetcatalog.ts","./src/services/importfidelity.test.ts","./src/services/importfidelity.ts","./src/services/mermaidparser.test.ts","./src/services/openflowdslexporter.test.ts","./src/services/openflowdslexporter.ts","./src/services/openflowdslparser.test.ts","./src/services/openflowroundtripgolden.test.ts","./src/services/openflowroundtripgoldenfixtures.ts","./src/services/operationfeedback.ts","./src/services/remainingfamiliesroundtrip.test.ts","./src/services/smartedgerouting.test.ts","./src/services/smartedgerouting.ts","./src/services/statediagramroundtrip.test.ts","./src/services/templates.selector.test.ts","./src/services/templates.ts","./src/services/zipextractor.ts","./src/services/ai/contextserializer.test.ts","./src/services/ai/contextserializer.ts","./src/services/analytics/analytics.ts","./src/services/analytics/analyticssettings.test.ts","./src/services/analytics/analyticssettings.ts","./src/services/analytics/surfaceanalyticsclient.ts","./src/services/architecturelint/defaultrules.ts","./src/services/architecturelint/ruleengine.test.ts","./src/services/architecturelint/ruleengine.ts","./src/services/architecturelint/rulelibrary.ts","./src/services/architecturelint/types.ts","./src/services/architecturelint/workspacerules.ts","./src/services/collaboration/bootstrap.test.ts","./src/services/collaboration/bootstrap.ts","./src/services/collaboration/canvasdiff.test.ts","./src/services/collaboration/canvasdiff.ts","./src/services/collaboration/comments.test.ts","./src/services/collaboration/comments.ts","./src/services/collaboration/contracts.test.ts","./src/services/collaboration/contracts.ts","./src/services/collaboration/hookutils.ts","./src/services/collaboration/operationlog.test.ts","./src/services/collaboration/operationlog.ts","./src/services/collaboration/presenceviewmodel.test.ts","./src/services/collaboration/presenceviewmodel.ts","./src/services/collaboration/reducer.test.ts","./src/services/collaboration/reducer.ts","./src/services/collaboration/roomconfig.ts","./src/services/collaboration/roomlink.test.ts","./src/services/collaboration/roomlink.ts","./src/services/collaboration/runtimecontroller.test.ts","./src/services/collaboration/runtimecontroller.ts","./src/services/collaboration/runtimehookutils.test.ts","./src/services/collaboration/runtimehookutils.ts","./src/services/collaboration/schemas.ts","./src/services/collaboration/session.test.ts","./src/services/collaboration/session.ts","./src/services/collaboration/storebridge.test.ts","./src/services/collaboration/storebridge.ts","./src/services/collaboration/transport.test.ts","./src/services/collaboration/transport.ts","./src/services/collaboration/transportfactory.test.ts","./src/services/collaboration/transportfactory.ts","./src/services/collaboration/types.ts","./src/services/collaboration/versioning.test.ts","./src/services/collaboration/versioning.ts","./src/services/collaboration/yjspeertransport.test.ts","./src/services/collaboration/yjspeertransport.ts","./src/services/diagramdiff/diffengine.ts","./src/services/elk-layout/boundaryfanout.ts","./src/services/elk-layout/determinism.ts","./src/services/elk-layout/options.ts","./src/services/elk-layout/types.ts","./src/services/export/cinematicbuildplan.test.ts","./src/services/export/cinematicbuildplan.ts","./src/services/export/cinematicexporttheme.test.ts","./src/services/export/cinematicexporttheme.ts","./src/services/export/cinematicrenderstate.test.ts","./src/services/export/cinematicrenderstate.ts","./src/services/export/formatting.ts","./src/services/export/mermaidbuilder.test.ts","./src/services/export/mermaidbuilder.ts","./src/services/export/pdfdocument.test.ts","./src/services/export/pdfdocument.ts","./src/services/export/plantumlbuilder.ts","./src/services/export/mermaid/architecturemermaid.ts","./src/services/export/mermaid/classdiagrammermaid.ts","./src/services/export/mermaid/erdiagrammermaid.ts","./src/services/export/mermaid/journeymermaid.ts","./src/services/export/mermaid/mindmapmermaid.ts","./src/services/export/mermaid/sequencemermaid.ts","./src/services/export/mermaid/statediagrammermaid.ts","./src/services/figma/edgehelpers.ts","./src/services/figma/iconhelpers.ts","./src/services/figma/nodelayers.ts","./src/services/figma/themehelpers.ts","./src/services/figmaimport/figmaapiclient.ts","./src/services/infrasync/dockercomposeparser.test.ts","./src/services/infrasync/dockercomposeparser.ts","./src/services/infrasync/infratodsl.test.ts","./src/services/infrasync/infratodsl.ts","./src/services/infrasync/kubernetesparser.test.ts","./src/services/infrasync/kubernetesparser.ts","./src/services/infrasync/terraformstateparser.test.ts","./src/services/infrasync/terraformstateparser.ts","./src/services/infrasync/types.ts","./src/services/mermaid/detectdiagramtype.test.ts","./src/services/mermaid/detectdiagramtype.ts","./src/services/mermaid/diagnosticformatting.test.ts","./src/services/mermaid/diagnosticformatting.ts","./src/services/mermaid/parsemermaidbytype.test.ts","./src/services/mermaid/parsemermaidbytype.ts","./src/services/mermaid/strictmodediagnosticspresentation.test.ts","./src/services/mermaid/strictmodediagnosticspresentation.ts","./src/services/mermaid/strictmodeguidance.test.ts","./src/services/mermaid/strictmodeguidance.ts","./src/services/mermaid/strictmodeuxregression.test.ts","./src/services/offline/registerappshellserviceworker.test.ts","./src/services/offline/registerappshellserviceworker.ts","./src/services/onboarding/config.ts","./src/services/onboarding/eventschemas.ts","./src/services/onboarding/events.test.ts","./src/services/onboarding/events.ts","./src/services/playback/contracts.test.ts","./src/services/playback/contracts.ts","./src/services/playback/model.test.ts","./src/services/playback/model.ts","./src/services/playback/studio.test.ts","./src/services/playback/studio.ts","./src/services/sequence/sequencemessage.test.ts","./src/services/sequence/sequencemessage.ts","./src/services/shapelibrary/bootstrap.test.ts","./src/services/shapelibrary/bootstrap.ts","./src/services/shapelibrary/ingestionpipeline.test.ts","./src/services/shapelibrary/ingestionpipeline.ts","./src/services/shapelibrary/manifestvalidation.test.ts","./src/services/shapelibrary/manifestvalidation.ts","./src/services/shapelibrary/providercatalog.test.ts","./src/services/shapelibrary/providercatalog.ts","./src/services/shapelibrary/registry.test.ts","./src/services/shapelibrary/registry.ts","./src/services/shapelibrary/starterpacks.ts","./src/services/shapelibrary/types.ts","./src/services/storage/fallbackstorage.ts","./src/services/storage/flowdocumentmodel.test.ts","./src/services/storage/flowdocumentmodel.ts","./src/services/storage/flowpersiststorage.test.ts","./src/services/storage/flowpersiststorage.ts","./src/services/storage/indexeddbhelpers.ts","./src/services/storage/indexeddbschema.test.ts","./src/services/storage/indexeddbschema.ts","./src/services/storage/indexeddbstatestorage.test.ts","./src/services/storage/indexeddbstatestorage.ts","./src/services/storage/localfirstrepository.ts","./src/services/storage/localfirstruntime.ts","./src/services/storage/persisteddocumentadapters.test.ts","./src/services/storage/persisteddocumentadapters.ts","./src/services/storage/persistencetypes.ts","./src/services/storage/snapshotstorage.test.ts","./src/services/storage/snapshotstorage.ts","./src/services/storage/storageruntime.test.ts","./src/services/storage/storageruntime.ts","./src/services/storage/storageschemas.test.ts","./src/services/storage/storageschemas.ts","./src/services/storage/storagetelemetry.test.ts","./src/services/storage/storagetelemetry.ts","./src/services/storage/storagetelemetrysink.test.ts","./src/services/storage/storagetelemetrysink.ts","./src/services/storage/uilocalstorage.test.ts","./src/services/storage/uilocalstorage.ts","./src/services/templatelibrary/registry.test.ts","./src/services/templatelibrary/registry.ts","./src/services/templatelibrary/startertemplates.assets.test.ts","./src/services/templatelibrary/startertemplates.test.ts","./src/services/templatelibrary/startertemplates.ts","./src/services/templatelibrary/templatefactories.ts","./src/services/templatelibrary/types.ts","./src/store/actionfactory.ts","./src/store/aisettings.test.ts","./src/store/aisettings.ts","./src/store/aisettingspersistence.ts","./src/store/aisettingsschemas.test.ts","./src/store/aisettingsschemas.ts","./src/store/canvashooks.ts","./src/store/createflowstore.test.ts","./src/store/createflowstore.ts","./src/store/createflowstorepersistoptions.ts","./src/store/createflowstorestate.ts","./src/store/defaults.ts","./src/store/designsystemhooks.ts","./src/store/documenthooks.ts","./src/store/documentstatesync.test.ts","./src/store/documentstatesync.ts","./src/store/editorpagehooks.ts","./src/store/historyhooks.ts","./src/store/historystate.ts","./src/store/persistence.test.ts","./src/store/persistence.ts","./src/store/persistenceschemas.ts","./src/store/selectionhooks.ts","./src/store/selectors.test.ts","./src/store/selectors.ts","./src/store/tabhooks.ts","./src/store/types.ts","./src/store/viewhooks.ts","./src/store/workspacedocumentmodel.test.ts","./src/store/workspacedocumentmodel.ts","./src/store/actions/createaiandselectionactions.ts","./src/store/actions/createcanvasactions.ts","./src/store/actions/createdesignsystemactions.ts","./src/store/actions/createhistoryactions.test.ts","./src/store/actions/createhistoryactions.ts","./src/store/actions/createlayeractions.ts","./src/store/actions/createtabactions.ts","./src/store/actions/createviewactions.ts","./src/store/actions/createworkspacedocumentactions.ts","./src/store/actions/synctabnodesedges.ts","./src/store/slices/createcanvaseditorslice.ts","./src/store/slices/createexperienceslice.ts","./src/store/slices/createworkspaceslice.ts"],"version":"5.8.3"}
\ No newline at end of file
+{"root":["./src/app.test.tsx","./src/app.tsx","./src/constants.test.ts","./src/constants.ts","./src/index.tsx","./src/store.test.ts","./src/store.ts","./src/theme.test.ts","./src/theme.ts","./src/app/routestate.test.ts","./src/app/routestate.ts","./src/components/annotationnode.tsx","./src/components/architecturerulespanel.tsx","./src/components/cinematicexportoverlay.tsx","./src/components/commandbar.test.tsx","./src/components/commandbar.tsx","./src/components/connectmenu.test.tsx","./src/components/connectmenu.tsx","./src/components/connectmenusections.tsx","./src/components/contextmenu.test.tsx","./src/components/contextmenu.tsx","./src/components/customconnectionline.test.tsx","./src/components/customconnectionline.tsx","./src/components/customedge.tsx","./src/components/customnode.handleinteraction.test.tsx","./src/components/customnode.tsx","./src/components/customnodecontent.tsx","./src/components/designsystem.integration.test.tsx","./src/components/diagramviewer.tsx","./src/components/errorboundary.tsx","./src/components/exportmenu.tsx","./src/components/exportmenupanel.test.tsx","./src/components/exportmenupanel.tsx","./src/components/flowcanvas.tsx","./src/components/floweditor.tsx","./src/components/floweditoremptystate.tsx","./src/components/floweditorlayoutoverlay.tsx","./src/components/floweditorpanels.test.tsx","./src/components/floweditorpanels.tsx","./src/components/flowtabs.test.tsx","./src/components/flowtabs.tsx","./src/components/groupnode.tsx","./src/components/homepage.integration.test.tsx","./src/components/homepage.tsx","./src/components/iconassetnodebody.tsx","./src/components/iconmap.test.tsx","./src/components/iconmap.ts","./src/components/imagenode.tsx","./src/components/importrecoverydialog.test.tsx","./src/components/importrecoverydialog.tsx","./src/components/inlinetexteditsurface.test.tsx","./src/components/inlinetexteditsurface.tsx","./src/components/keyboardshortcutsmodal.tsx","./src/components/languageselector.tsx","./src/components/markdownrenderer.tsx","./src/components/memoizedmarkdown.tsx","./src/components/navigationcontrols.tsx","./src/components/nodebadges.tsx","./src/components/nodechrome.tsx","./src/components/nodequickcreatebuttons.tsx","./src/components/nodeshapesvg.tsx","./src/components/nodetransformcontrols.tsx","./src/components/playbackcontrols.tsx","./src/components/propertiespanel.tsx","./src/components/rightrail.tsx","./src/components/sectionnode.tsx","./src/components/shareembedmodal.tsx","./src/components/sharemodal.test.tsx","./src/components/sharemodal.tsx","./src/components/sidebarshell.tsx","./src/components/snapshotspanel.test.tsx","./src/components/snapshotspanel.tsx","./src/components/studioaipanel.test.tsx","./src/components/studioaipanel.tsx","./src/components/studioaipanelsections.tsx","./src/components/studiocodepanel.test.tsx","./src/components/studiocodepanel.tsx","./src/components/studiopanel.test.tsx","./src/components/studiopanel.tsx","./src/components/studioplaybackpanel.test.tsx","./src/components/studioplaybackpanel.tsx","./src/components/swimlanenode.tsx","./src/components/textnode.tsx","./src/components/themetoggle.tsx","./src/components/toolbar.tsx","./src/components/tooltip.tsx","./src/components/topnav.tsx","./src/components/welcomemodal.tsx","./src/components/annotationtheme.ts","./src/components/container-nodes.handleinteraction.test.tsx","./src/components/editorsurfacetiers.ts","./src/components/handleinteraction.test.ts","./src/components/handleinteraction.ts","./src/components/handleinteractionusage.test.ts","./src/components/lightweight-nodes.handleinteraction.test.tsx","./src/components/markdownsyntax.ts","./src/components/nodehelpers.test.ts","./src/components/nodehelpers.ts","./src/components/sharemodalcontent.ts","./src/components/studioaicopy.ts","./src/components/studioaipanelexamples.ts","./src/components/transformdiagnostics.test.ts","./src/components/transformdiagnostics.ts","./src/components/useactivenodeselection.ts","./src/components/useexportmenu.test.tsx","./src/components/useexportmenu.ts","./src/components/settingsmodal/aisettings.tsx","./src/components/settingsmodal/canvassettings.tsx","./src/components/settingsmodal/generalsettings.test.tsx","./src/components/settingsmodal/generalsettings.tsx","./src/components/settingsmodal/settingsmodal.test.tsx","./src/components/settingsmodal/settingsmodal.tsx","./src/components/settingsmodal/shortcutssettings.tsx","./src/components/settingsmodal/ai/advancedendpointsection.tsx","./src/components/settingsmodal/ai/customheaderseditor.tsx","./src/components/settingsmodal/ai/privacysection.tsx","./src/components/settingsmodal/ai/providericon.tsx","./src/components/settingsmodal/ai/providersection.tsx","./src/components/settingsmodal/ai/step.tsx","./src/components/add-items/additemregistry.tsx","./src/components/app/docssiteredirect.test.tsx","./src/components/app/docssiteredirect.tsx","./src/components/app/mobileworkspacegate.test.tsx","./src/components/app/mobileworkspacegate.tsx","./src/components/app/routeloadingfallback.test.tsx","./src/components/app/routeloadingfallback.tsx","./src/components/app/mobileworkspacegatecopy.ts","./src/components/app/routeloadingcopy.ts","./src/components/architecture-lint/lintrulespanel.tsx","./src/components/architecture-lint/ruleform.tsx","./src/components/architecture-lint/visualeditor.tsx","./src/components/command-bar/assetsview.test.tsx","./src/components/command-bar/assetsview.tsx","./src/components/command-bar/codebaseimportpanels.tsx","./src/components/command-bar/codebaseimportsection.tsx","./src/components/command-bar/designsystemeditor.tsx","./src/components/command-bar/designsystemview.tsx","./src/components/command-bar/diagramminipreview.tsx","./src/components/command-bar/figmaimportpanel.tsx","./src/components/command-bar/importsurfaceprimitives.tsx","./src/components/command-bar/importview.tsx","./src/components/command-bar/importviewpanels.tsx","./src/components/command-bar/layersview.tsx","./src/components/command-bar/layoutview.tsx","./src/components/command-bar/pagesview.tsx","./src/components/command-bar/rootview.test.tsx","./src/components/command-bar/rootview.tsx","./src/components/command-bar/searchview.tsx","./src/components/command-bar/templatesview.test.tsx","./src/components/command-bar/templatesview.tsx","./src/components/command-bar/viewheader.tsx","./src/components/command-bar/visualsview.tsx","./src/components/command-bar/applycodechanges.ts","./src/components/command-bar/assetsviewconstants.ts","./src/components/command-bar/importdetection.test.ts","./src/components/command-bar/importdetection.ts","./src/components/command-bar/importnativeparsers.ts","./src/components/command-bar/importviewmodel.test.ts","./src/components/command-bar/importviewmodel.ts","./src/components/command-bar/mermaidimportparser.ts","./src/components/command-bar/searchquery.test.ts","./src/components/command-bar/searchquery.ts","./src/components/command-bar/types.ts","./src/components/command-bar/useaiviewstate.test.tsx","./src/components/command-bar/useaiviewstate.ts","./src/components/command-bar/usecloudassetcatalog.ts","./src/components/command-bar/usecommandbarcommands.test.tsx","./src/components/command-bar/usecommandbarcommands.tsx","./src/components/custom-edge/customedgewrapper.tsx","./src/components/custom-edge/edgemarkerdefs.tsx","./src/components/custom-edge/sequencemessageedge.test.tsx","./src/components/custom-edge/sequencemessageedge.tsx","./src/components/custom-edge/animatededgepresentation.test.ts","./src/components/custom-edge/animatededgepresentation.ts","./src/components/custom-edge/classrelationsemantics.test.ts","./src/components/custom-edge/classrelationsemantics.ts","./src/components/custom-edge/edgerendermode.ts","./src/components/custom-edge/pathutils.test.ts","./src/components/custom-edge/pathutils.ts","./src/components/custom-edge/pathutilsgeometry.ts","./src/components/custom-edge/pathutilssiblingrouting.ts","./src/components/custom-edge/pathutilstypes.ts","./src/components/custom-edge/relationroutingsemantics.test.ts","./src/components/custom-edge/relationroutingsemantics.ts","./src/components/custom-edge/standardedgemarkers.test.ts","./src/components/custom-edge/standardedgemarkers.ts","./src/components/custom-nodes/architecturenode.handleinteraction.test.tsx","./src/components/custom-nodes/architecturenode.tsx","./src/components/custom-nodes/browsernode.tsx","./src/components/custom-nodes/classentitynode.handleinteraction.test.tsx","./src/components/custom-nodes/classnode.tsx","./src/components/custom-nodes/entitynode.tsx","./src/components/custom-nodes/journeynode.tsx","./src/components/custom-nodes/mindmapnode.tsx","./src/components/custom-nodes/mobilenode.tsx","./src/components/custom-nodes/sequencenotenode.tsx","./src/components/custom-nodes/sequenceparticipantnode.tsx","./src/components/custom-nodes/structurednodehandles.tsx","./src/components/custom-nodes/visualnodes.handleinteraction.test.tsx","./src/components/custom-nodes/browservariantrenderer.tsx","./src/components/custom-nodes/mobilevariantrenderer.tsx","./src/components/custom-nodes/structuredlistnavigation.test.ts","./src/components/custom-nodes/structuredlistnavigation.ts","./src/components/custom-nodes/variantconstants.ts","./src/components/diagram-diff/diffmodebanner.tsx","./src/components/flow-canvas/flowcanvasalignmentguidesoverlay.tsx","./src/components/flow-canvas/flowcanvasoverlays.tsx","./src/components/flow-canvas/flowcanvassurface.test.tsx","./src/components/flow-canvas/flowcanvassurface.tsx","./src/components/flow-canvas/streamingoverlay.tsx","./src/components/flow-canvas/alignmentguides.test.ts","./src/components/flow-canvas/alignmentguides.ts","./src/components/flow-canvas/flowcanvasreactflowcontracts.ts","./src/components/flow-canvas/flowcanvastypes.test.ts","./src/components/flow-canvas/flowcanvastypes.tsx","./src/components/flow-canvas/largegraphsafetymode.test.ts","./src/components/flow-canvas/largegraphsafetymode.ts","./src/components/flow-canvas/nodechromecoverage.test.ts","./src/components/flow-canvas/pastehelpers.test.ts","./src/components/flow-canvas/pastehelpers.ts","./src/components/flow-canvas/useflowcanvasalignmentguides.ts","./src/components/flow-canvas/useflowcanvasconnectionstate.ts","./src/components/flow-canvas/useflowcanvascontextactions.ts","./src/components/flow-canvas/useflowcanvasdragdrop.ts","./src/components/flow-canvas/useflowcanvasinteractionlod.ts","./src/components/flow-canvas/useflowcanvasmenus.ts","./src/components/flow-canvas/useflowcanvasmenusandactions.test.tsx","./src/components/flow-canvas/useflowcanvasmenusandactions.ts","./src/components/flow-canvas/useflowcanvasnodedraghandlers.ts","./src/components/flow-canvas/useflowcanvaspaste.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.test.ts","./src/components/flow-canvas/useflowcanvasreactflowconfig.ts","./src/components/flow-canvas/useflowcanvasselectiontools.test.tsx","./src/components/flow-canvas/useflowcanvasselectiontools.ts","./src/components/flow-canvas/useflowcanvasviewstate.test.ts","./src/components/flow-canvas/useflowcanvasviewstate.ts","./src/components/flow-canvas/useflowcanvaszoomlod.ts","./src/components/flow-editor/collaborationpresenceoverlay.test.tsx","./src/components/flow-editor/collaborationpresenceoverlay.tsx","./src/components/flow-editor/floweditorchrome.tsx","./src/components/flow-editor/buildfloweditorcontrollerparams.ts","./src/components/flow-editor/buildfloweditorscreencontrollerparams.ts","./src/components/flow-editor/chromeproptypes.ts","./src/components/flow-editor/floweditorchromeprops.ts","./src/components/flow-editor/infradslapply.test.ts","./src/components/flow-editor/infradslapply.ts","./src/components/flow-editor/panelprops.test.ts","./src/components/flow-editor/panelprops.ts","./src/components/flow-editor/shouldexitstudioonselection.test.ts","./src/components/flow-editor/shouldexitstudioonselection.ts","./src/components/flow-editor/usecollaborationnodepositions.ts","./src/components/flow-editor/usefloweditorcontroller.ts","./src/components/flow-editor/usefloweditorinteractionbindings.ts","./src/components/flow-editor/usefloweditorpanelactions.ts","./src/components/flow-editor/usefloweditorpanelprops.ts","./src/components/flow-editor/usefloweditorruntime.ts","./src/components/flow-editor/usefloweditorscreenbehavior.ts","./src/components/flow-editor/usefloweditorscreenmodel.ts","./src/components/flow-editor/usefloweditorscreenstate.ts","./src/components/flow-editor/usefloweditorshellcontroller.test.tsx","./src/components/flow-editor/usefloweditorshellcontroller.ts","./src/components/flow-editor/usefloweditorstudiocontroller.test.tsx","./src/components/flow-editor/usefloweditorstudiocontroller.ts","./src/components/flow-editor/useinfradslapply.ts","./src/components/home/githubcard.tsx","./src/components/home/homedashboard.tsx","./src/components/home/homeflowdialogs.tsx","./src/components/home/homesettingsview.tsx","./src/components/home/homesidebar.tsx","./src/components/home/hometemplatesview.tsx","./src/components/home/sidebarfooter.tsx","./src/components/home/welcomemodalstate.ts","./src/components/icons/assetsicon.tsx","./src/components/icons/openflowlogo.tsx","./src/components/infra-sync/infrasyncpanel.test.tsx","./src/components/infra-sync/infrasyncpanel.tsx","./src/components/journey/journeyscorecontrol.test.tsx","./src/components/journey/journeyscorecontrol.tsx","./src/components/properties/bulknodeproperties.test.tsx","./src/components/properties/bulknodeproperties.tsx","./src/components/properties/bulknodepropertiesfamilysections.tsx","./src/components/properties/bulknodepropertiessections.tsx","./src/components/properties/bulknodepropertiesutilitysections.tsx","./src/components/properties/colorpicker.test.tsx","./src/components/properties/colorpicker.tsx","./src/components/properties/customcolorpopover.tsx","./src/components/properties/diagramnodepropertiesrouter.test.ts","./src/components/properties/diagramnodepropertiesrouter.tsx","./src/components/properties/edgeproperties.tsx","./src/components/properties/iconpicker.tsx","./src/components/properties/icontilepickerprimitives.tsx","./src/components/properties/imageupload.tsx","./src/components/properties/inspectorprimitives.tsx","./src/components/properties/nodeactionbuttons.tsx","./src/components/properties/nodecontentsection.tsx","./src/components/properties/nodeimagesettingssection.tsx","./src/components/properties/nodeproperties.test.tsx","./src/components/properties/nodeproperties.tsx","./src/components/properties/nodewireframevariantsection.tsx","./src/components/properties/propertysliderrow.tsx","./src/components/properties/segmentedchoice.tsx","./src/components/properties/shapeselector.tsx","./src/components/properties/swatchpicker.tsx","./src/components/properties/bulknodepropertiesmodel.test.ts","./src/components/properties/bulknodepropertiesmodel.ts","./src/components/properties/colorpickerutils.ts","./src/components/properties/propertyinputbehavior.test.ts","./src/components/properties/propertyinputbehavior.ts","./src/components/properties/sectionactionbuilder.ts","./src/components/properties/shared.ts","./src/components/properties/wireframevariants.ts","./src/components/properties/edge/architectureedgesemanticssection.tsx","./src/components/properties/edge/edgecolorsection.test.tsx","./src/components/properties/edge/edgecolorsection.tsx","./src/components/properties/edge/edgeconditionsection.tsx","./src/components/properties/edge/edgelabelsection.tsx","./src/components/properties/edge/edgerelationsection.test.tsx","./src/components/properties/edge/edgerelationsection.tsx","./src/components/properties/edge/edgeroutesection.test.tsx","./src/components/properties/edge/edgeroutesection.tsx","./src/components/properties/edge/edgestylesection.tsx","./src/components/properties/edge/sequencemessagesection.tsx","./src/components/properties/edge/architecturesemantics.test.ts","./src/components/properties/edge/architecturesemantics.ts","./src/components/properties/edge/edgecolorutils.ts","./src/components/properties/edge/edgelabelmodel.test.ts","./src/components/properties/edge/edgelabelmodel.ts","./src/components/properties/edge/errelationoptions.ts","./src/components/properties/families/architecturenodeproperties.test.tsx","./src/components/properties/families/architecturenodeproperties.tsx","./src/components/properties/families/architecturenodesection.tsx","./src/components/properties/families/classdiagramnodeproperties.tsx","./src/components/properties/families/classmemberlisteditor.tsx","./src/components/properties/families/classnodesection.tsx","./src/components/properties/families/erdiagramnodeproperties.tsx","./src/components/properties/families/entityfieldlisteditor.tsx","./src/components/properties/families/entitynodesection.tsx","./src/components/properties/families/journeynodeproperties.tsx","./src/components/properties/families/journeynodesection.tsx","./src/components/properties/families/mindmapnodeproperties.test.tsx","./src/components/properties/families/mindmapnodeproperties.tsx","./src/components/properties/families/sequencenodeproperties.tsx","./src/components/properties/families/sequencenodesection.tsx","./src/components/properties/families/specializednodecolorsections.test.tsx","./src/components/properties/families/structuredtextlisteditor.tsx","./src/components/properties/families/architectureoptions.ts","./src/components/studio-code-panel/usestudiocodepanelcontroller.test.tsx","./src/components/studio-code-panel/usestudiocodepanelcontroller.ts","./src/components/templates/templatepresentation.tsx","./src/components/toolbar/toolbaraddmenu.test.tsx","./src/components/toolbar/toolbaraddmenu.tsx","./src/components/toolbar/toolbaraddmenupanel.tsx","./src/components/toolbar/toolbarhistorycontrols.tsx","./src/components/toolbar/toolbarmodecontrols.tsx","./src/components/toolbar/toolbarbuttonstyles.ts","./src/components/top-nav/savestatusindicator.tsx","./src/components/top-nav/topnavactions.tsx","./src/components/top-nav/topnavbrand.tsx","./src/components/top-nav/topnavmenu.test.tsx","./src/components/top-nav/topnavmenu.tsx","./src/components/top-nav/topnavmenupanel.tsx","./src/components/top-nav/usetopnavstate.ts","./src/components/ui/button.tsx","./src/components/ui/collapsiblesection.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/searchfield.tsx","./src/components/ui/segmentedtabs.tsx","./src/components/ui/select.test.tsx","./src/components/ui/select.tsx","./src/components/ui/sidebaritem.tsx","./src/components/ui/slider.tsx","./src/components/ui/switch.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toastcontext.tsx","./src/components/ui/editorfieldstyles.ts","./src/config/aiproviders.test.ts","./src/config/aiproviders.ts","./src/config/rolloutflags.ts","./src/context/architecturelintcontext.tsx","./src/context/cinematicexportcontext.tsx","./src/context/diagramdiffcontext.tsx","./src/context/themecontext.tsx","./src/diagram-types/bootstrap.test.ts","./src/diagram-types/bootstrap.ts","./src/diagram-types/builtinplugins.ts","./src/diagram-types/builtinpropertypanels.ts","./src/diagram-types/registerbuiltinplugins.test.ts","./src/diagram-types/registerbuiltinplugins.ts","./src/diagram-types/registerbuiltinpropertypanels.test.ts","./src/diagram-types/registerbuiltinpropertypanels.ts","./src/diagram-types/architecture/fuzzcorpus.test.ts","./src/diagram-types/architecture/plugin.test.ts","./src/diagram-types/architecture/plugin.ts","./src/diagram-types/classdiagram/fuzzcorpus.test.ts","./src/diagram-types/classdiagram/plugin.test.ts","./src/diagram-types/classdiagram/plugin.ts","./src/diagram-types/core/contracts.ts","./src/diagram-types/core/index.ts","./src/diagram-types/core/propertypanels.test.ts","./src/diagram-types/core/propertypanels.ts","./src/diagram-types/core/registry.ts","./src/diagram-types/erdiagram/fuzzcorpus.test.ts","./src/diagram-types/erdiagram/plugin.test.ts","./src/diagram-types/erdiagram/plugin.ts","./src/diagram-types/flowchart/plugin.ts","./src/diagram-types/journey/fuzzcorpus.test.ts","./src/diagram-types/journey/plugin.test.ts","./src/diagram-types/journey/plugin.ts","./src/diagram-types/mindmap/fuzzcorpus.test.ts","./src/diagram-types/mindmap/plugin.test.ts","./src/diagram-types/mindmap/plugin.ts","./src/diagram-types/sequence/plugin.test.ts","./src/diagram-types/sequence/plugin.ts","./src/diagram-types/statediagram/plugin.test.ts","./src/diagram-types/statediagram/plugin.ts","./src/docs/docsroutes.ts","./src/docs/publicdocscatalog.js","./src/hooks/edgeconnectinteractions.test.ts","./src/hooks/edgeconnectinteractions.ts","./src/hooks/flowexportviewport.test.ts","./src/hooks/flowexportviewport.ts","./src/hooks/mindmaptopicactionrequest.ts","./src/hooks/nodelabeleditrequest.test.ts","./src/hooks/nodelabeleditrequest.ts","./src/hooks/nodequickcreaterequest.test.ts","./src/hooks/nodequickcreaterequest.ts","./src/hooks/snapshotpolicy.test.ts","./src/hooks/snapshotpolicy.ts","./src/hooks/useaigeneration.ts","./src/hooks/useanalyticspreference.ts","./src/hooks/useanimatededgeperformancewarning.test.ts","./src/hooks/useanimatededgeperformancewarning.ts","./src/hooks/useassetcatalog.ts","./src/hooks/usecinematicexport.ts","./src/hooks/useclipboardoperations.ts","./src/hooks/usedesignsystem.ts","./src/hooks/useedgeinteractions.ts","./src/hooks/useedgeoperations.ts","./src/hooks/usefloweditoractions.test.ts","./src/hooks/usefloweditoractions.ts","./src/hooks/usefloweditorcallbacks.ts","./src/hooks/usefloweditorcollaboration.test.ts","./src/hooks/usefloweditorcollaboration.ts","./src/hooks/usefloweditoruistate.ts","./src/hooks/useflowexport.ts","./src/hooks/useflowhistory.test.ts","./src/hooks/useflowhistory.ts","./src/hooks/useflowoperations.ts","./src/hooks/usegithubstars.ts","./src/hooks/useinfrasync.ts","./src/hooks/useinlinenodetextedit.test.ts","./src/hooks/useinlinenodetextedit.ts","./src/hooks/usekeyboardshortcuts.test.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/uselayoutoperations.ts","./src/hooks/usemarkdowneditor.ts","./src/hooks/usemenukeyboardnavigation.ts","./src/hooks/usemodifierkeys.test.ts","./src/hooks/usemodifierkeys.ts","./src/hooks/usenodeoperations.ts","./src/hooks/useplayback.ts","./src/hooks/useprovidershapepreview.ts","./src/hooks/userecentimports.ts","./src/hooks/useshiftheld.ts","./src/hooks/usesnapshots.ts","./src/hooks/usestaticexport.ts","./src/hooks/usestoragepressureguard.ts","./src/hooks/usestructuredlisteditor.ts","./src/hooks/usestyleclipboard.test.ts","./src/hooks/usestyleclipboard.ts","./src/hooks/ai-generation/chathistorystorage.test.ts","./src/hooks/ai-generation/chathistorystorage.ts","./src/hooks/ai-generation/codetoarchitecture.test.ts","./src/hooks/ai-generation/codetoarchitecture.ts","./src/hooks/ai-generation/codebaseanalyzer.test.ts","./src/hooks/ai-generation/codebaseanalyzer.ts","./src/hooks/ai-generation/codebaseparser.test.ts","./src/hooks/ai-generation/codebaseparser.ts","./src/hooks/ai-generation/codebasetonativediagram.test.ts","./src/hooks/ai-generation/codebasetonativediagram.ts","./src/hooks/ai-generation/graphcomposer.test.ts","./src/hooks/ai-generation/graphcomposer.ts","./src/hooks/ai-generation/nodeactionprompts.test.ts","./src/hooks/ai-generation/nodeactionprompts.ts","./src/hooks/ai-generation/openapiparser.test.ts","./src/hooks/ai-generation/openapiparser.ts","./src/hooks/ai-generation/openapitosequence.ts","./src/hooks/ai-generation/positionpreservingapply.test.ts","./src/hooks/ai-generation/positionpreservingapply.ts","./src/hooks/ai-generation/readiness.test.ts","./src/hooks/ai-generation/readiness.ts","./src/hooks/ai-generation/requestlifecycle.test.ts","./src/hooks/ai-generation/requestlifecycle.ts","./src/hooks/ai-generation/sqlparser.test.ts","./src/hooks/ai-generation/sqlparser.ts","./src/hooks/ai-generation/sqltoerd.ts","./src/hooks/ai-generation/streamingparser.ts","./src/hooks/ai-generation/streamingstore.ts","./src/hooks/ai-generation/terraformtocloud.ts","./src/hooks/edge-operations/utils.test.ts","./src/hooks/edge-operations/utils.ts","./src/hooks/flow-editor-actions/exporthandlers.test.ts","./src/hooks/flow-editor-actions/exporthandlers.ts","./src/hooks/flow-editor-actions/helpers.ts","./src/hooks/flow-editor-actions/layouthandlers.test.ts","./src/hooks/flow-editor-actions/layouthandlers.ts","./src/hooks/flow-export/diagramdocumenttransfer.test.ts","./src/hooks/flow-export/diagramdocumenttransfer.ts","./src/hooks/flow-export/exportcapture.test.ts","./src/hooks/flow-export/exportcapture.ts","./src/hooks/flow-operations/useflowcoreactions.ts","./src/hooks/node-operations/createconnectedsibling.ts","./src/hooks/node-operations/dragstopreconcilepolicy.test.ts","./src/hooks/node-operations/dragstopreconcilepolicy.ts","./src/hooks/node-operations/nodefactories.ts","./src/hooks/node-operations/routingduringdrag.test.ts","./src/hooks/node-operations/routingduringdrag.ts","./src/hooks/node-operations/sectionbounds.ts","./src/hooks/node-operations/sectionhittesting.ts","./src/hooks/node-operations/sectionoperations.ts","./src/hooks/node-operations/usearchitecturenodeoperations.test.ts","./src/hooks/node-operations/usearchitecturenodeoperations.ts","./src/hooks/node-operations/usemindmapnodeoperations.test.ts","./src/hooks/node-operations/usemindmapnodeoperations.ts","./src/hooks/node-operations/usenodedragoperations.test.ts","./src/hooks/node-operations/usenodedragoperations.ts","./src/hooks/node-operations/usenodeoperationadders.ts","./src/hooks/node-operations/utils.test.ts","./src/hooks/node-operations/utils.ts","./src/i18n/config.test.ts","./src/i18n/config.ts","./src/i18n/strictmodelocalecoverage.test.ts","./src/i18n/usedlocalecoverage.test.ts","./src/lib/architecturetemplatedata.ts","./src/lib/architecturetemplates.test.ts","./src/lib/architecturetemplates.ts","./src/lib/brand.ts","./src/lib/classmembers.ts","./src/lib/colorutils.ts","./src/lib/connectcreationpolicy.test.ts","./src/lib/connectcreationpolicy.ts","./src/lib/date.ts","./src/lib/designtokens.ts","./src/lib/devperformance.ts","./src/lib/entityfields.ts","./src/lib/ertoclassconversion.test.ts","./src/lib/ertoclassconversion.ts","./src/lib/exportfilename.test.ts","./src/lib/exportfilename.ts","./src/lib/flowminddslparserv2.test.ts","./src/lib/flowminddslparserv2.ts","./src/lib/fuzzymatch.ts","./src/lib/genericshapepolicy.ts","./src/lib/id.ts","./src/lib/index.ts","./src/lib/legacybranding.ts","./src/lib/logger.ts","./src/lib/mermaidparser.ts","./src/lib/mermaidparserhelpers.ts","./src/lib/mermaidparsermodel.ts","./src/lib/mindmaplayout.test.ts","./src/lib/mindmaplayout.ts","./src/lib/mindmaplayoutengine.ts","./src/lib/mindmaptree.ts","./src/lib/nodebulkediting.test.ts","./src/lib/nodebulkediting.ts","./src/lib/nodehandles.ts","./src/lib/nodeparent.ts","./src/lib/nodestyledata.test.ts","./src/lib/nodestyledata.ts","./src/lib/openflowdslparser.ts","./src/lib/openflowdslparserv2.ts","./src/lib/reactflowcompat.ts","./src/lib/reconnectedge.test.ts","./src/lib/reconnectedge.ts","./src/lib/relationsemantics.test.ts","./src/lib/relationsemantics.ts","./src/lib/releasestaleelkroutes.test.ts","./src/lib/releasestaleelkroutes.ts","./src/lib/result.ts","./src/lib/storagepressure.test.ts","./src/lib/storagepressure.ts","./src/lib/types.ts","./src/lib/xsssafety.test.tsx","./src/services/aligndistribute.ts","./src/services/aiservice.test.ts","./src/services/aiservice.ts","./src/services/aiserviceschemas.ts","./src/services/animatedexport.test.ts","./src/services/animatedexport.ts","./src/services/architectureroundtrip.test.ts","./src/services/assetcatalog.test.ts","./src/services/assetcatalog.ts","./src/services/assetpresentation.ts","./src/services/canonicalserialization.test.ts","./src/services/canonicalserialization.ts","./src/services/composediagramfordisplay.test.ts","./src/services/composediagramfordisplay.ts","./src/services/diagramdocument.test.ts","./src/services/diagramdocument.ts","./src/services/diagramdocumentschemas.ts","./src/services/domainlibrary.test.ts","./src/services/domainlibrary.ts","./src/services/elklayout.test.ts","./src/services/elklayout.ts","./src/services/exportservice.test.ts","./src/services/exportservice.ts","./src/services/figmaexportservice.ts","./src/services/flowchartroundtrip.test.ts","./src/services/geminiservice.ts","./src/services/geminisysteminstruction.ts","./src/services/gifencoder.test.ts","./src/services/gifencoder.ts","./src/services/githubfetcher.test.ts","./src/services/githubfetcher.ts","./src/services/iconassetcatalog.ts","./src/services/importfidelity.test.ts","./src/services/importfidelity.ts","./src/services/mermaidparser.test.ts","./src/services/openflowdslexporter.test.ts","./src/services/openflowdslexporter.ts","./src/services/openflowdslparser.test.ts","./src/services/openflowroundtripgolden.test.ts","./src/services/openflowroundtripgoldenfixtures.ts","./src/services/operationfeedback.ts","./src/services/remainingfamiliesroundtrip.test.ts","./src/services/smartedgerouting.test.ts","./src/services/smartedgerouting.ts","./src/services/statediagramroundtrip.test.ts","./src/services/templates.selector.test.ts","./src/services/templates.ts","./src/services/zipextractor.ts","./src/services/ai/contextserializer.test.ts","./src/services/ai/contextserializer.ts","./src/services/analytics/analytics.ts","./src/services/analytics/analyticssettings.test.ts","./src/services/analytics/analyticssettings.ts","./src/services/analytics/surfaceanalyticsclient.ts","./src/services/architecturelint/defaultrules.ts","./src/services/architecturelint/ruleengine.test.ts","./src/services/architecturelint/ruleengine.ts","./src/services/architecturelint/rulelibrary.ts","./src/services/architecturelint/types.ts","./src/services/architecturelint/workspacerules.ts","./src/services/collaboration/bootstrap.test.ts","./src/services/collaboration/bootstrap.ts","./src/services/collaboration/canvasdiff.test.ts","./src/services/collaboration/canvasdiff.ts","./src/services/collaboration/comments.test.ts","./src/services/collaboration/comments.ts","./src/services/collaboration/contracts.test.ts","./src/services/collaboration/contracts.ts","./src/services/collaboration/hookutils.ts","./src/services/collaboration/operationlog.test.ts","./src/services/collaboration/operationlog.ts","./src/services/collaboration/presenceviewmodel.test.ts","./src/services/collaboration/presenceviewmodel.ts","./src/services/collaboration/reducer.test.ts","./src/services/collaboration/reducer.ts","./src/services/collaboration/roomconfig.ts","./src/services/collaboration/roomlink.test.ts","./src/services/collaboration/roomlink.ts","./src/services/collaboration/runtimecontroller.test.ts","./src/services/collaboration/runtimecontroller.ts","./src/services/collaboration/runtimehookutils.test.ts","./src/services/collaboration/runtimehookutils.ts","./src/services/collaboration/schemas.ts","./src/services/collaboration/session.test.ts","./src/services/collaboration/session.ts","./src/services/collaboration/storebridge.test.ts","./src/services/collaboration/storebridge.ts","./src/services/collaboration/transport.test.ts","./src/services/collaboration/transport.ts","./src/services/collaboration/transportfactory.test.ts","./src/services/collaboration/transportfactory.ts","./src/services/collaboration/types.ts","./src/services/collaboration/versioning.test.ts","./src/services/collaboration/versioning.ts","./src/services/collaboration/yjspeertransport.test.ts","./src/services/collaboration/yjspeertransport.ts","./src/services/diagramdiff/diffengine.ts","./src/services/elk-layout/boundaryfanout.ts","./src/services/elk-layout/determinism.ts","./src/services/elk-layout/options.ts","./src/services/elk-layout/types.ts","./src/services/export/cinematicbuildplan.test.ts","./src/services/export/cinematicbuildplan.ts","./src/services/export/cinematicexport.ts","./src/services/export/cinematicexporttheme.test.ts","./src/services/export/cinematicexporttheme.ts","./src/services/export/cinematicrenderstate.test.ts","./src/services/export/cinematicrenderstate.ts","./src/services/export/formatting.ts","./src/services/export/mermaidbuilder.test.ts","./src/services/export/mermaidbuilder.ts","./src/services/export/pdfdocument.test.ts","./src/services/export/pdfdocument.ts","./src/services/export/plantumlbuilder.ts","./src/services/export/mermaid/architecturemermaid.ts","./src/services/export/mermaid/classdiagrammermaid.ts","./src/services/export/mermaid/erdiagrammermaid.ts","./src/services/export/mermaid/journeymermaid.ts","./src/services/export/mermaid/mindmapmermaid.ts","./src/services/export/mermaid/sequencemermaid.ts","./src/services/export/mermaid/statediagrammermaid.ts","./src/services/figma/edgehelpers.ts","./src/services/figma/iconhelpers.ts","./src/services/figma/nodelayers.ts","./src/services/figma/themehelpers.ts","./src/services/figmaimport/figmaapiclient.ts","./src/services/flowpilot/assetgrounding.test.ts","./src/services/flowpilot/assetgrounding.ts","./src/services/flowpilot/prompting.ts","./src/services/flowpilot/responsepolicy.test.ts","./src/services/flowpilot/responsepolicy.ts","./src/services/flowpilot/skills.ts","./src/services/flowpilot/thread.ts","./src/services/flowpilot/types.ts","./src/services/infrasync/dockercomposeparser.test.ts","./src/services/infrasync/dockercomposeparser.ts","./src/services/infrasync/infratodsl.test.ts","./src/services/infrasync/infratodsl.ts","./src/services/infrasync/kubernetesparser.test.ts","./src/services/infrasync/kubernetesparser.ts","./src/services/infrasync/terraformstateparser.test.ts","./src/services/infrasync/terraformstateparser.ts","./src/services/infrasync/types.ts","./src/services/mermaid/detectdiagramtype.test.ts","./src/services/mermaid/detectdiagramtype.ts","./src/services/mermaid/diagnosticformatting.test.ts","./src/services/mermaid/diagnosticformatting.ts","./src/services/mermaid/parsemermaidbytype.test.ts","./src/services/mermaid/parsemermaidbytype.ts","./src/services/mermaid/strictmodediagnosticspresentation.test.ts","./src/services/mermaid/strictmodediagnosticspresentation.ts","./src/services/mermaid/strictmodeguidance.test.ts","./src/services/mermaid/strictmodeguidance.ts","./src/services/mermaid/strictmodeuxregression.test.ts","./src/services/offline/registerappshellserviceworker.test.ts","./src/services/offline/registerappshellserviceworker.ts","./src/services/onboarding/config.ts","./src/services/onboarding/eventschemas.ts","./src/services/onboarding/events.test.ts","./src/services/onboarding/events.ts","./src/services/playback/contracts.test.ts","./src/services/playback/contracts.ts","./src/services/playback/model.test.ts","./src/services/playback/model.ts","./src/services/playback/studio.test.ts","./src/services/playback/studio.ts","./src/services/sequence/sequencemessage.test.ts","./src/services/sequence/sequencemessage.ts","./src/services/shapelibrary/bootstrap.test.ts","./src/services/shapelibrary/bootstrap.ts","./src/services/shapelibrary/ingestionpipeline.test.ts","./src/services/shapelibrary/ingestionpipeline.ts","./src/services/shapelibrary/manifestvalidation.test.ts","./src/services/shapelibrary/manifestvalidation.ts","./src/services/shapelibrary/providercatalog.test.ts","./src/services/shapelibrary/providercatalog.ts","./src/services/shapelibrary/registry.test.ts","./src/services/shapelibrary/registry.ts","./src/services/shapelibrary/starterpacks.ts","./src/services/shapelibrary/types.ts","./src/services/storage/fallbackstorage.ts","./src/services/storage/flowdocumentmodel.test.ts","./src/services/storage/flowdocumentmodel.ts","./src/services/storage/flowpersiststorage.test.ts","./src/services/storage/flowpersiststorage.ts","./src/services/storage/indexeddbhelpers.ts","./src/services/storage/indexeddbschema.test.ts","./src/services/storage/indexeddbschema.ts","./src/services/storage/indexeddbstatestorage.test.ts","./src/services/storage/indexeddbstatestorage.ts","./src/services/storage/localfirstrepository.ts","./src/services/storage/localfirstruntime.ts","./src/services/storage/persisteddocumentadapters.test.ts","./src/services/storage/persisteddocumentadapters.ts","./src/services/storage/persistencetypes.ts","./src/services/storage/snapshotstorage.test.ts","./src/services/storage/snapshotstorage.ts","./src/services/storage/storageruntime.test.ts","./src/services/storage/storageruntime.ts","./src/services/storage/storageschemas.test.ts","./src/services/storage/storageschemas.ts","./src/services/storage/storagetelemetry.test.ts","./src/services/storage/storagetelemetry.ts","./src/services/storage/storagetelemetrysink.test.ts","./src/services/storage/storagetelemetrysink.ts","./src/services/storage/uilocalstorage.test.ts","./src/services/storage/uilocalstorage.ts","./src/services/templatelibrary/registry.test.ts","./src/services/templatelibrary/registry.ts","./src/services/templatelibrary/startertemplates.assets.test.ts","./src/services/templatelibrary/startertemplates.test.ts","./src/services/templatelibrary/startertemplates.ts","./src/services/templatelibrary/templatefactories.ts","./src/services/templatelibrary/types.ts","./src/store/actionfactory.ts","./src/store/aisettings.test.ts","./src/store/aisettings.ts","./src/store/aisettingspersistence.ts","./src/store/aisettingsschemas.test.ts","./src/store/aisettingsschemas.ts","./src/store/canvashooks.ts","./src/store/createflowstore.test.ts","./src/store/createflowstore.ts","./src/store/createflowstorepersistoptions.ts","./src/store/createflowstorestate.ts","./src/store/defaults.ts","./src/store/designsystemhooks.ts","./src/store/documenthooks.ts","./src/store/documentstatesync.test.ts","./src/store/documentstatesync.ts","./src/store/editorpagehooks.ts","./src/store/historyhooks.ts","./src/store/historystate.ts","./src/store/persistence.test.ts","./src/store/persistence.ts","./src/store/persistenceschemas.ts","./src/store/selectionhooks.ts","./src/store/selectors.test.ts","./src/store/selectors.ts","./src/store/tabhooks.ts","./src/store/types.ts","./src/store/viewhooks.ts","./src/store/workspacedocumentmodel.test.ts","./src/store/workspacedocumentmodel.ts","./src/store/actions/createaiandselectionactions.ts","./src/store/actions/createcanvasactions.ts","./src/store/actions/createdesignsystemactions.ts","./src/store/actions/createhistoryactions.test.ts","./src/store/actions/createhistoryactions.ts","./src/store/actions/createlayeractions.ts","./src/store/actions/createtabactions.ts","./src/store/actions/createviewactions.ts","./src/store/actions/createworkspacedocumentactions.ts","./src/store/actions/synctabnodesedges.ts","./src/store/slices/createcanvaseditorslice.ts","./src/store/slices/createexperienceslice.ts","./src/store/slices/createworkspaceslice.ts"],"version":"5.8.3"}
\ No newline at end of file
diff --git a/web/src/components/landing/AnimationExportSection.tsx b/web/src/components/landing/AnimationExportSection.tsx
index c28cd605..32569258 100644
--- a/web/src/components/landing/AnimationExportSection.tsx
+++ b/web/src/components/landing/AnimationExportSection.tsx
@@ -16,7 +16,7 @@ const FEATURE_CARDS = [
},
{
title: 'Pristine 60fps Native Exports',
- sub: 'Render natively to MP4 or high-quality GIFs without leaving the browser environment.',
+ sub: 'Render natively to MP4 without leaving the browser environment.',
icon: Film,
},
] as const;
@@ -60,7 +60,7 @@ export function AnimationExportSection(): React.ReactElement {
- MP4 & GIF
+ MP4 EXPORT
{/* The Main UI Container */}