From ff18a9bd244f8b3ff03b3095f3de8b89b355f16c Mon Sep 17 00:00:00 2001 From: DimitriGilbert Date: Thu, 3 Jul 2025 22:55:18 +0200 Subject: [PATCH 1/4] [vibe] orchestration renderer premise (AI generated workflows) --- .gitignore | 2 + src/App.tsx | 2 + .../common/OrchestrationBlockRenderer.tsx | 219 +++++++++++++++++ .../OrchestrationBlockRendererModule.ts | 223 ++++++++++++++++++ src/store/workflow.store.ts | 1 - 5 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 src/components/LiteChat/common/OrchestrationBlockRenderer.tsx create mode 100644 src/controls/modules/OrchestrationBlockRendererModule.ts diff --git a/.gitignore b/.gitignore index 7aaaba68..1fddde9a 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ src.*.d todo.*.md public/versions/ **/*.bak +build +release diff --git a/src/App.tsx b/src/App.tsx index 08630c27..8a23ebf5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -78,6 +78,7 @@ import { ChartBlockRendererModule } from "@/controls/modules/ChartBlockRendererM import { JsRunnableBlockRendererModule } from "@/controls/modules/JsRunnableBlockRendererModule"; import { PythonRunnableBlockRendererModule } from "@/controls/modules/PythonRunnableBlockRendererModule"; import { BeatBlockRendererModule } from "@/controls/modules/BeatBlockRendererModule"; +import { OrchestrationBlockRendererModule } from "@/controls/modules/OrchestrationBlockRendererModule"; // Define the application's specific control module registration order HERE const controlModulesToRegister: ControlModuleConstructor[] = [ @@ -127,6 +128,7 @@ const controlModulesToRegister: ControlModuleConstructor[] = [ JsRunnableBlockRendererModule, PythonRunnableBlockRendererModule, BeatBlockRendererModule, + OrchestrationBlockRendererModule, // Canvas Action Controls CopyActionControlModule, // For InteractionCard header FoldInteractionControlModule, // For InteractionCard header diff --git a/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx b/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx new file mode 100644 index 00000000..cdd17470 --- /dev/null +++ b/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx @@ -0,0 +1,219 @@ +import React, { useState, useMemo, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { Button } from "@/components/ui/button"; +import { CodeIcon, PlayIcon, SaveIcon, EditIcon, ChevronDownIcon, ChevronRightIcon, AlertCircleIcon } from "lucide-react"; +import { toast } from "sonner"; +import { emitter } from "@/lib/litechat/event-emitter"; +import { PersistenceService } from "@/services/persistence.service"; +import type { WorkflowTemplate } from "@/types/litechat/workflow"; +import { uiEvent } from "@/types/litechat/events/ui.events"; +import { usePromptTemplateStore } from "@/store/prompt-template.store"; +import { nanoid } from "nanoid"; + +interface OrchestrationBlockRendererProps { + code: string; + isStreaming?: boolean; +} + +function parseWorkflow(code: string): { workflow?: WorkflowTemplate; error?: string } { + try { + const parsed = JSON.parse(code); + // Basic validation (reuse logic from WorkflowRawEditor) + if (!parsed.id || typeof parsed.id !== "string") return { error: 'Workflow must have a valid "id" field (string)' }; + if (!parsed.name || typeof parsed.name !== "string") return { error: 'Workflow must have a valid "name" field (string)' }; + if (!parsed.description || typeof parsed.description !== "string") return { error: 'Workflow must have a valid "description" field (string)' }; + if (!Array.isArray(parsed.steps)) return { error: 'Workflow must have a "steps" field that is an array' }; + if (!parsed.createdAt || typeof parsed.createdAt !== "string") return { error: 'Workflow must have a valid "createdAt" field (ISO string)' }; + if (!parsed.updatedAt || typeof parsed.updatedAt !== "string") return { error: 'Workflow must have a valid "updatedAt" field (ISO string)' }; + // Steps validation (minimal) + for (let i = 0; i < parsed.steps.length; i++) { + const step = parsed.steps[i]; + if (!step.id || typeof step.id !== "string") return { error: `Step ${i + 1}: must have a valid "id" field (string)` }; + if (!step.name || typeof step.name !== "string") return { error: `Step ${i + 1}: must have a valid "name" field (string)` }; + if (!step.type || typeof step.type !== "string") return { error: `Step ${i + 1}: must have a valid "type" field (string)` }; + } + return { workflow: parsed as WorkflowTemplate }; + } catch (e) { + return { error: `Invalid workflow JSON: ${(e as Error).message}` }; + } +} + +function checkIfWorkflowNeedsInput(workflow: WorkflowTemplate): boolean { + // Check if trigger needs input + if (workflow.triggerType === 'custom' && !workflow.triggerPrompt) { + return true; // Custom trigger without prompt + } + if (workflow.triggerType === 'template' && workflow.triggerRef) { + // Check if template has variables that need values + const templates = usePromptTemplateStore.getState().promptTemplates; + const template = templates.find(t => t.id === workflow.triggerRef); + if (template && template.variables && template.variables.length > 0) { + // Check if templateVariables has all required values + const hasAllValues = template.variables.every(variable => { + const value = workflow.templateVariables?.[variable.name]; + return value !== undefined && value !== null && value !== ''; + }); + if (!hasAllValues) return true; + } + } + // Workflow is ready to run + return false; +} + +export const OrchestrationBlockRenderer: React.FC = ({ code, isStreaming = false }) => { + const { t } = useTranslation(); + const [isFolded, setIsFolded] = useState(isStreaming); + const [showCode, setShowCode] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const { workflow, error } = useMemo(() => parseWorkflow(code), [code]); + + const handleRun = useCallback(async () => { + if (!workflow) return; + const needsInput = checkIfWorkflowNeedsInput(workflow); + if (needsInput) { + // Open the workflow builder modal for configuration + emitter.emit(uiEvent.openModalRequest, { + modalId: "workflowBuilderModal", + modalProps: { workflow }, + }); + return; + } + // Always create a temporary workflow in the DB with __TEMP__ prefix + const tempWorkflow = { + ...workflow, + id: nanoid(), + name: `__TEMP__-${workflow.name}`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + let tempId = tempWorkflow.id; + try { + await PersistenceService.saveWorkflow(tempWorkflow); + // Run using the event system, passing the temp workflow's ID + emitter.emit("workflow.run.orchestration", { workflowId: tempId }); + toast.success(`Workflow "${workflow.name}" started!`); + } catch (e) { + toast.error("Failed to run workflow: " + (e instanceof Error ? e.message : String(e))); + // Attempt cleanup if save failed + try { await PersistenceService.deleteWorkflow(tempId); } catch {} + return; + } + // Always delete the temp workflow after a short delay to allow runner to fetch it + setTimeout(async () => { + try { await PersistenceService.deleteWorkflow(tempId); } catch {} + }, 5000); + }, [workflow]); + + const handleSave = useCallback(async () => { + if (!workflow) return; + setIsSaving(true); + try { + // Check if this workflow already exists in the DB + let existing: WorkflowTemplate | null = null; + try { + existing = await PersistenceService.loadWorkflows().then(ws => ws.find(w => w.id === workflow.id) || null); + } catch (e) { + existing = null; + } + let workflowToSave: WorkflowTemplate; + if (existing) { + // Editing existing: preserve all step IDs + workflowToSave = { ...workflow }; + } else { + // New workflow: assign IDs according to the strict plan + workflowToSave = { + ...workflow, + steps: workflow.steps.map(step => { + if ( + (step.type === 'prompt' && step.templateId) || + (step.type === 'agent-task' && step.taskId) + ) { + // Preserve the step id as-is + return { ...step }; + } else { + // Assign a new nanoid for all other cases + return { ...step, id: nanoid() }; + } + }) + }; + } + await PersistenceService.saveWorkflow(workflowToSave); + toast.success("Workflow saved to library"); + } catch (e) { + toast.error("Failed to save workflow: " + (e instanceof Error ? e.message : String(e))); + } finally { + setIsSaving(false); + } + }, [workflow]); + + const handleEdit = useCallback(() => { + if (!workflow) return; + // Open the workflow builder modal with the workflow as modalProps + emitter.emit(uiEvent.openModalRequest, { + modalId: "workflowBuilderModal", + modalProps: { workflow }, + }); + }, [workflow]); + + if (error) { + return ( +
+ + {error} +
+ ); + } + + if (!workflow) return null; + + return ( +
+ {/* Header */} +
+
+ + {workflow.name} + Orchestration Workflow +
+
+ + + + +
+
+ {/* Preview Area */} + {!isFolded && ( +
+
{workflow.description}
+
+
Steps:
+
    + {workflow.steps.map((step) => ( +
  1. + {step.name} [{step.type}] +
  2. + ))} +
+
+
+ )} + {/* Code View */} + {showCode && ( +
+          {code}
+        
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/controls/modules/OrchestrationBlockRendererModule.ts b/src/controls/modules/OrchestrationBlockRendererModule.ts new file mode 100644 index 00000000..8eb14302 --- /dev/null +++ b/src/controls/modules/OrchestrationBlockRendererModule.ts @@ -0,0 +1,223 @@ +import type { ControlModule } from "@/types/litechat/control"; +import type { LiteChatModApi } from "@/types/litechat/modding"; +import type { BlockRenderer, BlockRendererContext } from "@/types/litechat/canvas/block-renderer"; +import { OrchestrationBlockRenderer } from "@/components/LiteChat/common/OrchestrationBlockRenderer"; +import React from "react"; +import { usePromptTemplateStore } from "@/store/prompt-template.store"; +import { emitter } from "@/lib/litechat/event-emitter"; +import { promptTemplateEvent } from "@/types/litechat/events/prompt-template.events"; +import type { PromptTemplate } from "@/types/litechat/prompt-template"; + +const TEMPLATE_LIBRARY_RULE_ID = "orchestration-template-library-control-rule"; +const MAIN_RULE_ID = "core-block-renderer-orchestration-control-rule"; + +function formatTemplateList(templates: PromptTemplate[]): string { + // Output a plain JSON array for AI parsing + return JSON.stringify(templates.map(t => ({ + id: t.id, + name: t.name, + type: t.type || "prompt", + variables: t.variables, + description: t.description, + tags: t.tags, + isShortcut: t.isShortcut, + isPublic: t.isPublic + })), null, 2); +} + +function getCanonicalExamples(): string { + return ` +### Example 1: Mars Color Explanation + +\u0060\u0060\u0060orchestration +{ + "id": "KTP4-gVubXkznj-TNw-qZ", + "name": "why is mars red", + "description": "explain color of mars and simulate", + "steps": [ + { + "id": "step_1750238527413_0", + "name": "make it python", + "type": "transform", + "modelId": "wDGr_e9ASG1e4J7O6OlHp:meta-llama/llama-4-scout:free", + "transformMappings": { + "the_question": "$.workflow.triggerPrompt", + "explanation": "$.outputs[0]" + } + }, + { + "id": "sghWWQADFTEbZBMnEql24", + "name": "simulate", + "type": "prompt", + "modelId": "wDGr_e9ASG1e4J7O6OlHp:moonshotai/kimi-dev-72b:free", + "templateId": "mL_lWwDbqSMUR2TbdFKNG" + } + ], + "createdAt": "2025-06-18T15:19:36.587Z", + "updatedAt": "2025-06-28T13:38:16.769Z", + "triggerType": "template", + "triggerPrompt": "Why is Mars red ?", + "triggerRef": "h4uShBJAE5235hK4hrGBc", + "templateVariables": { + "what": "Mars", + "color": "red" + }, + "isShortcut": true +} +\u0060\u0060\u0060 + +### Example 2: Python Lint/Validate Output + +\u0060\u0060\u0060orchestration +{ + "id": "lint-python-eg-1", + "name": "Python Output Lint", + "description": "Lint and validate a generated Python code output.", + "steps": [ + { + "id": "step1", + "type": "prompt", + "modelId": "wDGr_e9ASG1e4J7O6OlHp:meta-llama/llama-4-scout:free", + "templateId": "", + "outputVar": "generated_code" + }, + { + "id": "step2", + "type": "code", + "language": "python", + "code": "import ast\ntry:\n ast.parse(generated_code)\n print('VALID')\nexcept Exception as e:\n print('INVALID:', e)", + "inputVar": "generated_code", + "outputVar": "lint_result" + } + ], + "createdAt": "2025-06-18T15:19:36.587Z", + "updatedAt": "2025-06-28T13:38:16.769Z", + "triggerType": "template", + "triggerPrompt": "Write a Python function to compute the nth Fibonacci number.", + "triggerRef": "", + "templateVariables": { + "n": 10 + }, + "isShortcut": false +} +\u0060\u0060\u0060`; +} + +function getMainRuleContent(): string { + return `# Orchestration Workflow Block (LLM Guide) + +You can output a workflow definition using a markdown code block with the language identifier \`orchestration\`. + +## Step Types +- **prompt**: Runs a prompt template. Fields: \`templateId\`, \`modelId\`, \`outputVar\` (optional). +- **transform**: Runs a transform step. Fields: \`modelId\`, \`transformMappings\` (object mapping output fields). +- **code**: Runs code in a language. Fields: \`language\`, \`code\`, \`inputVar\`, \`outputVar\`. + +## Referencing Templates +- Use \`templateId\` to reference a prompt, agent, or task from the [Orchestration Template Library] control rule (see below). +- Do **not** include the full template list in your block—just reference by ID. + +## Variable Passing +- Use \`outputVar\` to name the output of a step. +- Use \`inputVar\` to consume the output of a previous step. +- You can pass objects as input/output. + +## Canonical Examples +${getCanonicalExamples()} + +## Template Discovery +- To discover available prompt/agent/task templates, consult the always-up-to-date [Orchestration Template Library] control rule. +`; +} + +export class OrchestrationBlockRendererModule implements ControlModule { + readonly id = "core-block-renderer-orchestration"; + private unregisterCallback?: () => void; + private unregisterMainRuleCallback?: () => void; + private unregisterTemplateRuleCallback?: () => void; + private modApi?: LiteChatModApi; + private templateListenerUnsub?: () => void; + + async initialize(): Promise {} + + register(modApi: LiteChatModApi): void { + if (this.unregisterCallback) { + console.warn(`[${this.id}] Already registered. Skipping.`); + return; + } + this.modApi = modApi; + const orchestrationBlockRenderer: BlockRenderer = { + id: this.id, + supportedLanguages: ["orchestration"], + priority: 10, + renderer: (context: BlockRendererContext) => { + return React.createElement(OrchestrationBlockRenderer, { + code: context.code, + isStreaming: context.isStreaming, + }); + }, + }; + this.unregisterCallback = modApi.registerBlockRenderer(orchestrationBlockRenderer); + this.registerOrUpdateRules(); + // Listen for template changes and update the template rule in real time + this.templateListenerUnsub = () => { + emitter.off(promptTemplateEvent.promptTemplatesChanged, this.handleTemplatesChanged); + emitter.off(promptTemplateEvent.promptTemplateAdded, this.handleTemplatesChanged); + emitter.off(promptTemplateEvent.promptTemplateUpdated, this.handleTemplatesChanged); + emitter.off(promptTemplateEvent.promptTemplateDeleted, this.handleTemplatesChanged); + }; + this.handleTemplatesChanged = this.handleTemplatesChanged.bind(this); + emitter.on(promptTemplateEvent.promptTemplatesChanged, this.handleTemplatesChanged); + emitter.on(promptTemplateEvent.promptTemplateAdded, this.handleTemplatesChanged); + emitter.on(promptTemplateEvent.promptTemplateUpdated, this.handleTemplatesChanged); + emitter.on(promptTemplateEvent.promptTemplateDeleted, this.handleTemplatesChanged); + } + + private handleTemplatesChanged() { + this.registerOrUpdateRules(); + } + + private registerOrUpdateRules() { + const templates = usePromptTemplateStore.getState().promptTemplates; + // Unregister previous rules if present + if (this.unregisterMainRuleCallback) this.unregisterMainRuleCallback(); + if (this.unregisterTemplateRuleCallback) this.unregisterTemplateRuleCallback(); + // Register main rule + this.unregisterMainRuleCallback = this.modApi?.registerRule({ + id: MAIN_RULE_ID, + name: "Orchestration Workflow Block Control", + content: getMainRuleContent(), + type: "control", + alwaysOn: true, + moduleId: this.id, + }); + // Register template library rule + this.unregisterTemplateRuleCallback = this.modApi?.registerRule({ + id: TEMPLATE_LIBRARY_RULE_ID, + name: "Orchestration Template Library", + content: formatTemplateList(templates), + type: "control", + alwaysOn: true, + moduleId: this.id, + }); + } + + destroy(): void { + if (this.unregisterCallback) { + this.unregisterCallback(); + this.unregisterCallback = undefined; + } + if (this.unregisterMainRuleCallback) { + this.unregisterMainRuleCallback(); + this.unregisterMainRuleCallback = undefined; + } + if (this.unregisterTemplateRuleCallback) { + this.unregisterTemplateRuleCallback(); + this.unregisterTemplateRuleCallback = undefined; + } + if (this.templateListenerUnsub) { + this.templateListenerUnsub(); + this.templateListenerUnsub = undefined; + } + } +} \ No newline at end of file diff --git a/src/store/workflow.store.ts b/src/store/workflow.store.ts index ef1c0e66..d7d328ee 100644 --- a/src/store/workflow.store.ts +++ b/src/store/workflow.store.ts @@ -3,7 +3,6 @@ import { immer } from "zustand/middleware/immer"; import type { WorkflowRun, WorkflowRunStatus } from "@/types/litechat/workflow"; import type { RegisteredActionHandler } from "@/types/litechat/control"; import { workflowEvent, type WorkflowEventPayloads } from "@/types/litechat/events/workflow.events"; -// import { emitter } from "@/lib/litechat/event-emitter"; export interface WorkflowState { activeRun: WorkflowRun | null; From 9b390d7381f3ab6266ab834f43eeab6ea9bc8d7a Mon Sep 17 00:00:00 2001 From: DimitriGilbert Date: Fri, 4 Jul 2025 00:22:59 +0200 Subject: [PATCH 2/4] [vibe] cursor auto model got me to unsub ! get you shit together guys ! --- .../common/OrchestrationBlockRenderer.tsx | 282 ++++++++++-------- .../OrchestrationBlockRendererModule.ts | 16 +- 2 files changed, 170 insertions(+), 128 deletions(-) diff --git a/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx b/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx index cdd17470..43d669ae 100644 --- a/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx +++ b/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; -import { CodeIcon, PlayIcon, SaveIcon, EditIcon, ChevronDownIcon, ChevronRightIcon, AlertCircleIcon } from "lucide-react"; +import { CodeIcon, PlayIcon, SaveIcon, EditIcon, AlertCircleIcon } from "lucide-react"; import { toast } from "sonner"; import { emitter } from "@/lib/litechat/event-emitter"; import { PersistenceService } from "@/services/persistence.service"; @@ -9,16 +9,26 @@ import type { WorkflowTemplate } from "@/types/litechat/workflow"; import { uiEvent } from "@/types/litechat/events/ui.events"; import { usePromptTemplateStore } from "@/store/prompt-template.store"; import { nanoid } from "nanoid"; +import { workflowEvent } from "@/types/litechat/events/workflow.events"; +import { useControlRegistryStore } from "@/store/control.store"; +import { useSettingsStore } from "@/store/settings.store"; +import { useShallow } from "zustand/react/shallow"; +import { CodeBlockRenderer } from "./CodeBlockRenderer"; +import type { CanvasControl } from "@/types/litechat/canvas/control"; +import { useConversationStore } from "@/store/conversation.store"; -interface OrchestrationBlockRendererProps { +interface OrchestrationBlockProps { code: string; - isStreaming?: boolean; + isStreaming: boolean; } function parseWorkflow(code: string): { workflow?: WorkflowTemplate; error?: string } { + if (!code.trim()) { + return {}; + } try { const parsed = JSON.parse(code); - // Basic validation (reuse logic from WorkflowRawEditor) + // Basic validation if (!parsed.id || typeof parsed.id !== "string") return { error: 'Workflow must have a valid "id" field (string)' }; if (!parsed.name || typeof parsed.name !== "string") return { error: 'Workflow must have a valid "name" field (string)' }; if (!parsed.description || typeof parsed.description !== "string") return { error: 'Workflow must have a valid "description" field (string)' }; @@ -34,185 +44,217 @@ function parseWorkflow(code: string): { workflow?: WorkflowTemplate; error?: str } return { workflow: parsed as WorkflowTemplate }; } catch (e) { + // Only show parse errors if not streaming, to avoid flicker with incomplete JSON return { error: `Invalid workflow JSON: ${(e as Error).message}` }; } } function checkIfWorkflowNeedsInput(workflow: WorkflowTemplate): boolean { - // Check if trigger needs input if (workflow.triggerType === 'custom' && !workflow.triggerPrompt) { - return true; // Custom trigger without prompt + return true; } if (workflow.triggerType === 'template' && workflow.triggerRef) { - // Check if template has variables that need values const templates = usePromptTemplateStore.getState().promptTemplates; const template = templates.find(t => t.id === workflow.triggerRef); - if (template && template.variables && template.variables.length > 0) { - // Check if templateVariables has all required values - const hasAllValues = template.variables.every(variable => { + if (template?.variables?.length) { + return !template.variables.every(variable => { const value = workflow.templateVariables?.[variable.name]; return value !== undefined && value !== null && value !== ''; }); - if (!hasAllValues) return true; } } - // Workflow is ready to run return false; } -export const OrchestrationBlockRenderer: React.FC = ({ code, isStreaming = false }) => { - const { t } = useTranslation(); - const [isFolded, setIsFolded] = useState(isStreaming); +export const OrchestrationBlockRenderer: React.FC = ({ code, isStreaming = false }) => { + const { t } = useTranslation('renderers'); + const { foldStreamingCodeBlocks } = useSettingsStore( + useShallow((state) => ({ + foldStreamingCodeBlocks: state.foldStreamingCodeBlocks, + })) + ); + const conversationId = useConversationStore(state => state.selectedItemId); + + const [isFolded, setIsFolded] = useState(isStreaming ? foldStreamingCodeBlocks : false); const [showCode, setShowCode] = useState(false); const [isSaving, setIsSaving] = useState(false); - const { workflow, error } = useMemo(() => parseWorkflow(code), [code]); - const handleRun = useCallback(async () => { - if (!workflow) return; - const needsInput = checkIfWorkflowNeedsInput(workflow); - if (needsInput) { - // Open the workflow builder modal for configuration + const { workflow, error } = useMemo(() => { + if (isStreaming) { + const trimmedCode = code.trim(); + const isLikelyJson = (trimmedCode.startsWith('{') && trimmedCode.endsWith('}')); + if (!isLikelyJson) return {}; // Wait for more complete structure + const openBraces = (trimmedCode.match(/[{[]/g) || []).length; + const closeBraces = (trimmedCode.match(/[}\]]/g) || []).length; + if (openBraces !== closeBraces) return {}; // Still incomplete + } + return parseWorkflow(code); + }, [code, isStreaming]); + + const canvasControls = useControlRegistryStore( + useShallow((state) => Object.values(state.canvasControls)) + ); + + const toggleFold = () => setIsFolded((prev) => !prev); + const toggleView = () => setShowCode((prev) => !prev); + + const handleRun = useCallback(() => { + if (!workflow || !conversationId) return; + if (checkIfWorkflowNeedsInput(workflow)) { emitter.emit(uiEvent.openModalRequest, { modalId: "workflowBuilderModal", modalProps: { workflow }, }); - return; + } else { + emitter.emit(workflowEvent.startRequest, { template: workflow, initialPrompt: workflow.triggerPrompt || "", conversationId }); + toast.success(t('orchestrationBlock.runRequestSent', { name: workflow.name })); } - // Always create a temporary workflow in the DB with __TEMP__ prefix - const tempWorkflow = { - ...workflow, - id: nanoid(), - name: `__TEMP__-${workflow.name}`, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - let tempId = tempWorkflow.id; - try { - await PersistenceService.saveWorkflow(tempWorkflow); - // Run using the event system, passing the temp workflow's ID - emitter.emit("workflow.run.orchestration", { workflowId: tempId }); - toast.success(`Workflow "${workflow.name}" started!`); - } catch (e) { - toast.error("Failed to run workflow: " + (e instanceof Error ? e.message : String(e))); - // Attempt cleanup if save failed - try { await PersistenceService.deleteWorkflow(tempId); } catch {} - return; - } - // Always delete the temp workflow after a short delay to allow runner to fetch it - setTimeout(async () => { - try { await PersistenceService.deleteWorkflow(tempId); } catch {} - }, 5000); - }, [workflow]); + }, [workflow, t, conversationId]); const handleSave = useCallback(async () => { if (!workflow) return; setIsSaving(true); try { - // Check if this workflow already exists in the DB - let existing: WorkflowTemplate | null = null; - try { - existing = await PersistenceService.loadWorkflows().then(ws => ws.find(w => w.id === workflow.id) || null); - } catch (e) { - existing = null; - } - let workflowToSave: WorkflowTemplate; - if (existing) { - // Editing existing: preserve all step IDs - workflowToSave = { ...workflow }; - } else { - // New workflow: assign IDs according to the strict plan - workflowToSave = { - ...workflow, - steps: workflow.steps.map(step => { - if ( - (step.type === 'prompt' && step.templateId) || - (step.type === 'agent-task' && step.taskId) - ) { - // Preserve the step id as-is - return { ...step }; - } else { - // Assign a new nanoid for all other cases - return { ...step, id: nanoid() }; - } - }) - }; - } + const existing = await PersistenceService.loadWorkflows().then(ws => ws.find(w => w.id === workflow.id) || null); + const workflowToSave: WorkflowTemplate = existing ? { ...workflow } : { + ...workflow, + steps: workflow.steps.map(step => ({ ...step, id: nanoid() })) + }; await PersistenceService.saveWorkflow(workflowToSave); - toast.success("Workflow saved to library"); + toast.success(t('orchestrationBlock.saveSuccess')); } catch (e) { - toast.error("Failed to save workflow: " + (e instanceof Error ? e.message : String(e))); + toast.error(t('orchestrationBlock.saveFailed', { message: (e as Error).message })); } finally { setIsSaving(false); } - }, [workflow]); + }, [workflow, t]); const handleEdit = useCallback(() => { if (!workflow) return; - // Open the workflow builder modal with the workflow as modalProps emitter.emit(uiEvent.openModalRequest, { modalId: "workflowBuilderModal", modalProps: { workflow }, }); }, [workflow]); - if (error) { + const renderSlotForCodeBlock = useCallback( + ( + targetSlotName: CanvasControl["targetSlot"], + currentCode: string, + currentLang?: string, + currentIsFolded?: boolean, + currentToggleFold?: () => void + ): React.ReactNode[] => { + return canvasControls + .filter(c => c.type === "codeblock" && c.targetSlot === targetSlotName && c.renderer) + .map((control) => ( + + {control.renderer!({ + codeBlockContent: currentCode, + codeBlockLang: currentLang, + isFolded: currentIsFolded, + toggleFold: currentToggleFold, + canvasContextType: "codeblock", + })} + + )); + }, + [canvasControls] + ); + + const foldedPreviewText = useMemo(() => { + if (!code) return ""; + return code.split("\n").slice(0, 3).join("\n"); + }, [code]); + + const codeBlockHeaderActions = renderSlotForCodeBlock( + "codeblock-header-actions", + code, + "orchestration", + isFolded, + toggleFold + ); + + if (error && !isStreaming) { return ( -
- - {error} +
+
+ +
{t('orchestrationBlock.dataErrorTitle')}
+
+
{error}
); } - if (!workflow) return null; + if (!workflow) { + return isStreaming ? ( +
+
+
+
{t('orchestrationBlock.header')}
+
+
+
{t('orchestrationBlock.waitingForData')}
+
+ ) : null; + } return ( -
- {/* Header */} -
-
- - {workflow.name} - Orchestration Workflow +
+
+
+
{t('orchestrationBlock.header')}
+
+ {codeBlockHeaderActions} +
-
- - - -
- {/* Preview Area */} + {!isFolded && ( -
-
{workflow.description}
-
-
Steps:
-
    - {workflow.steps.map((step) => ( -
  1. - {step.name} [{step.type}] -
  2. - ))} -
-
+
+ {showCode ? ( + + ) : ( +
+
{workflow.name}
+
{workflow.description}
+
+
{t('orchestrationBlock.stepsHeader')}:
+
    + {workflow.steps.map((step) => ( +
  1. + {step.name}{' '} + [{step.type}] +
  2. + ))} +
+
+
+ )}
)} - {/* Code View */} - {showCode && ( -
-          {code}
-        
+ + {isFolded && ( +
+
+            {foldedPreviewText}
+          
+
)}
); diff --git a/src/controls/modules/OrchestrationBlockRendererModule.ts b/src/controls/modules/OrchestrationBlockRendererModule.ts index 8eb14302..05e309ff 100644 --- a/src/controls/modules/OrchestrationBlockRendererModule.ts +++ b/src/controls/modules/OrchestrationBlockRendererModule.ts @@ -106,20 +106,20 @@ function getCanonicalExamples(): string { function getMainRuleContent(): string { return `# Orchestration Workflow Block (LLM Guide) -You can output a workflow definition using a markdown code block with the language identifier \`orchestration\`. +You can output a workflow definition using a markdown code block with the language identifier \u0060orchestration\u0060. ## Step Types -- **prompt**: Runs a prompt template. Fields: \`templateId\`, \`modelId\`, \`outputVar\` (optional). -- **transform**: Runs a transform step. Fields: \`modelId\`, \`transformMappings\` (object mapping output fields). -- **code**: Runs code in a language. Fields: \`language\`, \`code\`, \`inputVar\`, \`outputVar\`. +- **prompt**: Runs a prompt template. Fields: \u0060templateId\u0060, \u0060modelId\u0060, \u0060outputVar\u0060 (optional). +- **transform**: Runs a transform step. Fields: \u0060modelId\u0060, \u0060transformMappings\u0060 (object mapping output fields). +- **code**: Runs code in a language. Fields: \u0060language\u0060, \u0060code\u0060, \u0060inputVar\u0060, \u0060outputVar\u0060. ## Referencing Templates -- Use \`templateId\` to reference a prompt, agent, or task from the [Orchestration Template Library] control rule (see below). +- Use \u0060templateId\u0060 to reference a prompt, agent, or task from the [Orchestration Template Library] control rule (see below). - Do **not** include the full template list in your block—just reference by ID. ## Variable Passing -- Use \`outputVar\` to name the output of a step. -- Use \`inputVar\` to consume the output of a previous step. +- Use \u0060outputVar\u0060 to name the output of a step. +- Use \u0060inputVar\u0060 to consume the output of a previous step. - You can pass objects as input/output. ## Canonical Examples @@ -153,7 +153,7 @@ export class OrchestrationBlockRendererModule implements ControlModule { renderer: (context: BlockRendererContext) => { return React.createElement(OrchestrationBlockRenderer, { code: context.code, - isStreaming: context.isStreaming, + isStreaming: context.isStreaming ?? false, }); }, }; From 89c3c47ac33ac802e7095dfb4f92bdf6e73fefa1 Mon Sep 17 00:00:00 2001 From: DimitriGilbert Date: Fri, 4 Jul 2025 00:35:56 +0200 Subject: [PATCH 3/4] [vibe] align --- .../common/OrchestrationBlockRenderer.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx b/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx index 43d669ae..11e80447 100644 --- a/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx +++ b/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx @@ -19,7 +19,7 @@ import { useConversationStore } from "@/store/conversation.store"; interface OrchestrationBlockProps { code: string; - isStreaming: boolean; + isStreaming?: boolean; } function parseWorkflow(code: string): { workflow?: WorkflowTemplate; error?: string } { @@ -82,11 +82,12 @@ export const OrchestrationBlockRenderer: React.FC = ({ const { workflow, error } = useMemo(() => { if (isStreaming) { const trimmedCode = code.trim(); - const isLikelyJson = (trimmedCode.startsWith('{') && trimmedCode.endsWith('}')); - if (!isLikelyJson) return {}; // Wait for more complete structure - const openBraces = (trimmedCode.match(/[{[]/g) || []).length; - const closeBraces = (trimmedCode.match(/[}\]]/g) || []).length; - if (openBraces !== closeBraces) return {}; // Still incomplete + // Try parsing directly and return empty if it fails during streaming + try { + JSON.parse(trimmedCode); + } catch { + return {}; // Still incomplete or invalid + } } return parseWorkflow(code); }, [code, isStreaming]); @@ -164,8 +165,11 @@ export const OrchestrationBlockRenderer: React.FC = ({ const foldedPreviewText = useMemo(() => { if (!code) return ""; + if (workflow) { + return `${workflow.name}\n${workflow.description}\n${workflow.steps.length} steps`; + } return code.split("\n").slice(0, 3).join("\n"); - }, [code]); + }, [code, workflow]); const codeBlockHeaderActions = renderSlotForCodeBlock( "codeblock-header-actions", From 3dc6759becc21f5957020bc1ee0fcf787d0ae51b Mon Sep 17 00:00:00 2001 From: DimitriGilbert Date: Fri, 4 Jul 2025 00:59:04 +0200 Subject: [PATCH 4/4] bugbot --- docs/block-renderer-system.md | 468 +++++++++--------- .../common/OrchestrationBlockRenderer.tsx | 4 +- 2 files changed, 227 insertions(+), 245 deletions(-) diff --git a/docs/block-renderer-system.md b/docs/block-renderer-system.md index ccd87d36..69b72385 100644 --- a/docs/block-renderer-system.md +++ b/docs/block-renderer-system.md @@ -26,318 +26,300 @@ The Block Renderer System allows you to: 3. **Rendering**: The selected renderer processes the code and returns React components 4. **Fallback**: If no specific renderer is found, falls back to the default code renderer -## Creating a Custom Block Renderer +## How to Create a Custom Block Renderer: A Detailed Guide -### Step 1: Define Your Renderer +This guide provides a comprehensive walkthrough for creating a new block renderer, from basic setup to advanced features, based on an analysis of all existing renderers in the project. + +### Step 1: Create the Renderer Module + +First, create a new TypeScript file for your control module in `src/controls/modules/`. The convention is to name it `[MyRenderer]BlockRendererModule.ts`. + +This module is responsible for registering your renderer with the application. + +**Example: `src/controls/modules/MyCustomBlockRendererModule.ts`** ```typescript -// src/controls/modules/MyCustomRendererModule.ts import type { ControlModule } from "@/types/litechat/control"; import type { LiteChatModApi } from "@/types/litechat/modding"; import type { BlockRenderer, BlockRendererContext } from "@/types/litechat/canvas/block-renderer"; +import { MyCustomBlockRenderer } from "@/components/LiteChat/common/MyCustomBlockRenderer"; import React from "react"; -export class MyCustomRendererModule implements ControlModule { - readonly id = "my-custom-renderer"; +export class MyCustomBlockRendererModule implements ControlModule { + // A unique ID for your module + readonly id = "my-custom-block-renderer"; + + // Store the unregister function for cleanup private unregisterCallback?: () => void; - async initialize(): Promise { - // Setup if needed + // Optional: If your renderer needs a control rule for the AI + private unregisterRuleCallback?: () => void; + + async initialize(modApi: LiteChatModApi): Promise { + // Perform any async setup here if needed. + // Most renderers will not need this. } register(modApi: LiteChatModApi): void { - const customRenderer: BlockRenderer = { + // Define the renderer object + const myCustomRenderer: BlockRenderer = { id: this.id, - supportedLanguages: ["mylang", "custom"], // Languages this renderer handles - priority: 10, // Higher priority = preferred over other renderers + // The language identifiers this renderer will handle + supportedLanguages: ["my-lang", "custom-data"], + // Higher priority renderers are chosen over lower ones for the same language + priority: 15, + // The function that returns the React component renderer: (context: BlockRendererContext) => { - return React.createElement(MyCustomComponent, { + // Pass the context to your component + return React.createElement(MyCustomBlockRenderer, { code: context.code, - lang: context.lang, - filepath: context.filepath, - isStreaming: context.isStreaming, + isStreaming: context.isStreaming ?? false, + // Pass other context properties as needed + interactionId: context.interactionId, blockId: context.blockId, }); }, }; - this.unregisterCallback = modApi.registerBlockRenderer(customRenderer); + // Register the renderer and store the cleanup function + this.unregisterCallback = modApi.registerBlockRenderer(myCustomRenderer); + + // (Optional) Register a control rule to guide the AI + const controlRuleContent = ` + When you need to display data in a special format, use the 'my-lang' code block. + ```my-lang + { "key": "value" } + ``` + `; + this.unregisterRuleCallback = modApi.registerRule({ + id: `${this.id}-rule`, + name: "My Custom Renderer Guide", + content: controlRuleContent, + type: "control", + alwaysOn: true, // Or false if it should be user-configurable + moduleId: this.id, + }); } destroy(): void { - if (this.unregisterCallback) { - this.unregisterCallback(); - this.unregisterCallback = undefined; - } + // Cleanup on module destruction + this.unregisterCallback?.(); + this.unregisterRuleCallback?.(); } } ``` -### Step 2: Create Your Renderer Component - -```typescript -interface MyCustomComponentProps { - code: string; - lang?: string; - filepath?: string; - isStreaming?: boolean; - blockId?: string; -} - -const MyCustomComponent: React.FC = ({ - code, - lang, - filepath, - isStreaming, - blockId, -}) => { - // Your custom rendering logic here - return ( -
-
-
- {lang?.toUpperCase() || "CUSTOM"} -
- {filepath && ( -
{filepath}
- )} -
-
- {/* Your custom rendering */} -
{code}
-
-
- ); -}; -``` - -### Step 3: Register Your Module +### Step 2: Create the Renderer React Component -Add your module to `src/App.tsx`: +Next, create the React component that will render your block. Place this file in `src/components/LiteChat/common/` with the name `[MyRenderer]BlockRenderer.tsx`. -```typescript -import { MyCustomRendererModule } from "@/controls/modules/MyCustomRendererModule"; +This component receives props from the module and handles the actual rendering logic. -const controlModulesToRegister: ControlModuleConstructor[] = [ - // ... existing modules - MyCustomRendererModule, // Add your module - // ... rest of modules -]; -``` +**Key UI/UX Patterns to Follow:** -## BlockRenderer Interface +- **Main Container:** Wrap your component in a `div` with `className="code-block-container group/codeblock my-4 max-w-full"`. +- **Header:** Implement a consistent header that is visible on hover. +- **Actions:** Provide actions like "view code" or "download" in the header. +- **Folding:** Support a folded state, especially for streaming or large content. +- **Loading/Error States:** Display clear loading indicators and user-friendly error messages. -```typescript -interface BlockRenderer { - id: string; - // Languages this renderer handles (e.g., ["mermaid"], ["typescript", "javascript"]) - // Empty array or undefined means it handles all languages (fallback renderer) - supportedLanguages?: string[]; - // Priority for renderer selection (higher = more priority) - priority?: number; - // The actual renderer component - renderer: (context: BlockRendererContext) => React.ReactNode; - // Optional lifecycle hooks - onMounted?: (context: BlockRendererContext & { element: HTMLElement }) => void; - onUnmounted?: (context: BlockRendererContext) => void; -} -``` - -### BlockRendererContext +**Example: `src/components/LiteChat/common/MyCustomBlockRenderer.tsx`** ```typescript -interface BlockRendererContext { - lang: string | undefined; +import React, { useState, useMemo, useCallback, memo } from "react"; +import { useTranslation } from "react-i18next"; +import { useSettingsStore } from "@/store/settings.store"; +import { useShallow } from "zustand/react/shallow"; +import { CodeBlockRenderer } from "./CodeBlockRenderer"; +import { AlertCircleIcon, Loader2Icon, CodeIcon, ImageIcon } from "lucide-react"; + +interface MyCustomBlockRendererProps { code: string; - filepath?: string; - isStreaming?: boolean; - blockId?: string; + isStreaming: boolean; } -``` -## Renderer Selection Logic - -The system selects renderers using the following priority: - -1. **Specific Language Match**: Renderers that explicitly support the block's language -2. **Priority**: Among matching renderers, higher priority wins -3. **Fallback**: Renderers with no `supportedLanguages` (or empty array) serve as fallbacks -4. **Default**: If no renderers match, uses a simple pre/code fallback - -### Example Priority Scenarios - -```typescript -// Scenario: Rendering a "mermaid" block - -// Renderer A: Mermaid-specific (priority 10) -{ supportedLanguages: ["mermaid"], priority: 10 } - -// Renderer B: General fallback (priority 0) -{ supportedLanguages: undefined, priority: 0 } - -// Result: Renderer A is selected (specific match + higher priority) -``` +const MyCustomBlockRendererComponent: React.FC = ({ code, isStreaming }) => { + const { t } = useTranslation('renderers'); + const { foldStreamingCodeBlocks } = useSettingsStore( + useShallow((state) => ({ foldStreamingCodeBlocks: state.foldStreamingCodeBlocks })) + ); -## Built-in Renderers + // State Management + const [isFolded, setIsFolded] = useState(isStreaming ? foldStreamingCodeBlocks : false); + const [showCode, setShowCode] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [parsedData, setParsedData] = useState(null); + + // Parsing Logic (runs when code changes and is not folded/streaming) + const parseData = useCallback(() => { + if (!code.trim() || isFolded || isStreaming) return; + setIsLoading(true); + setError(null); + try { + const data = JSON.parse(code); + setParsedData(data); + } catch (e) { + setError(e instanceof Error ? e.message : "Invalid data format"); + } finally { + setIsLoading(false); + } + }, [code, isFolded, isStreaming]); -### CodeBlockRendererModule -- **Languages**: All (fallback) -- **Priority**: 0 -- **Features**: Syntax highlighting, copy/fold actions, file path display + useEffect(() => { + parseData(); + }, [parseData]); -### MermaidBlockRendererModule -- **Languages**: `["mermaid"]` -- **Priority**: 10 -- **Features**: Diagram rendering, error handling, loading states + // Event Handlers + const toggleFold = () => setIsFolded(p => !p); + const toggleView = () => setShowCode(p => !p); -## Advanced Features + // Memoized preview for folded state + const foldedPreviewText = useMemo(() => code.split('\n').slice(0, 3).join('\n'), [code]); -### Dynamic Language Support + return ( +
+ {/* Consistent Header */} +
+
+
{t('myCustomBlock.header')}
+
+
+ +
+
-You can create renderers that handle multiple related languages: + {/* Content Area */} + {!isFolded && ( +
+ {showCode ? ( + + ) : ( + <> + {isLoading &&
} + {error && ( +
+ +
{error}
+
+ )} + {parsedData && !isLoading && !error && ( +
+ {/* Your custom visualization here */} +
{JSON.stringify(parsedData, null, 2)}
+
+ )} + + )} +
+ )} -```typescript -const multiLangRenderer: BlockRenderer = { - id: "web-languages", - supportedLanguages: ["html", "css", "javascript", "typescript"], - priority: 5, - renderer: (context) => { - // Handle different web languages with specialized rendering - switch (context.lang) { - case "html": - return renderHtml(context); - case "css": - return renderCss(context); - default: - return renderJavaScript(context); - } - }, + {/* Folded State */} + {isFolded && ( +
+
{foldedPreviewText}
+
+ )} +
+ ); }; -``` - -### Conditional Registration - -Register renderers based on settings or conditions: -```typescript -register(modApi: LiteChatModApi): void { - const enableAdvancedRendering = this.getAdvancedRenderingSetting(); - - if (enableAdvancedRendering) { - this.unregisterCallback = modApi.registerBlockRenderer(advancedRenderer); - } -} +export const MyCustomBlockRenderer = memo(MyCustomBlockRendererComponent); ``` -### Interactive Renderers +### Step 3: Register Your Module in `App.tsx` -Create renderers with interactive features: +Finally, import and add your new module to the `controlModulesToRegister` array in `src/App.tsx`. The order matters for dependencies, but for most block renderers, the order is not critical. It's good practice to group them together. ```typescript -const interactiveRenderer: BlockRenderer = { - id: "interactive-sql", - supportedLanguages: ["sql"], - priority: 8, - renderer: (context) => { - return React.createElement(InteractiveSqlRenderer, { - query: context.code, - onExecute: (query) => { - // Handle SQL execution - }, - }); - }, -}; -``` - -## Best Practices - -### 1. Follow UI Patterns -- Use consistent CSS classes (`code-block-container`, `code-block-header`) -- Support the existing action system (copy, fold, download) -- Maintain responsive design +// src/App.tsx +import { MyCustomBlockRendererModule } from "@/controls/modules/MyCustomBlockRendererModule"; -### 2. Handle Edge Cases -- Empty code blocks -- Invalid syntax -- Streaming content -- Large content +const controlModulesToRegister: ControlModuleConstructor[] = [ + // ... other modules + CodeBlockRendererModule, + MermaidBlockRendererModule, + ChartBlockRendererModule, + FlowBlockRendererModule, + FormedibleBlockRendererModule, + OrchestrationBlockRendererModule, + JsRunnableBlockRendererModule, + PythonRunnableBlockRendererModule, + MyCustomBlockRendererModule, // Add your new module here + // ... rest of modules +]; +``` -### 3. Performance Considerations -- Use React.memo for expensive renderers -- Implement lazy loading for heavy visualizations -- Debounce updates during streaming +### Advanced Concepts and Best Practices -### 4. Error Handling -- Gracefully handle rendering errors -- Provide fallback rendering -- Log errors for debugging +#### Handling Streaming Content -### Example Error Handling +If `isStreaming` is true, your renderer should be careful about parsing. The code is likely incomplete. +- **Fold by default:** Set `useState(isStreaming ? foldStreamingCodeBlocks : false)`. +- **Debounce parsing:** Use a `setTimeout` in an effect to avoid parsing on every single character change. +- **Validate structure:** Before parsing, check if the code has a chance of being valid (e.g., starts with `{` and ends with `}`). ```typescript -renderer: (context) => { - try { - return renderComplexVisualization(context); - } catch (error) { - console.error(`[${this.id}] Rendering error:`, error); - // Fallback to simple rendering - return React.createElement("pre", {}, context.code); +// From ChartBlockRenderer.tsx +const parseChart = useCallback(async () => { + if (isStreaming) { + const trimmedCode = code.trim(); + if (!(trimmedCode.startsWith('{') && trimmedCode.endsWith('}'))) { + return; // Incomplete, don't attempt to parse + } } -}, -``` - -## Integration with Canvas Controls - -Block renderers work seamlessly with existing canvas controls: + // ... parsing logic +}, [code, isStreaming]); -- **Copy Actions**: Automatically available in block headers -- **Fold Controls**: Integrated with streaming settings -- **Download Actions**: Support file export functionality +useEffect(() => { + const handle = setTimeout(parseChart, isStreaming ? 300 : 0); + return () => clearTimeout(handle); +}, [code, isStreaming, parseChart]); +``` -## Event System Integration +#### Integrating with Canvas Controls -Block renderers can interact with the event system: +Your renderer can and should integrate with other canvas controls, like "Copy" or "Edit". This is done by rendering slots. ```typescript -// Listen for theme changes -modApi.on(settingsEvent.themeChanged, (payload) => { - // Update renderer styling - this.updateTheme(payload.theme); -}); - -// Emit custom events -modApi.emit("blockRenderer.customEvent", { - rendererId: this.id, - data: customData, -}); -``` - -## Testing Your Renderer - -1. **Create test content** with your target language -2. **Verify selection logic** - ensure your renderer is chosen -3. **Test edge cases** - empty blocks, invalid syntax, streaming -4. **Check responsiveness** - test on different screen sizes -5. **Validate accessibility** - ensure proper ARIA labels and keyboard navigation +// From CodeBlockRenderer.tsx +const canvasControls = useControlRegistryStore( + useShallow((state) => Object.values(state.canvasControls)) +); + +const renderSlotForCodeBlock = useCallback((targetSlotName, ...) => { + return canvasControls + .filter(c => c.type === "codeblock" && c.targetSlot === targetSlotName) + .map(control => { + const context: CanvasControlRenderContext = { ... }; + return {control.renderer!(context)}; + }); +}, [canvasControls]); -## Migration from Direct Renderers +// In your JSX: +
+ {renderSlotForCodeBlock("codeblock-header-actions", ...)} +
+``` -If you have existing direct renderer usage, migrate to the new system: +#### Creating Interactive Renderers (e.g., Runnable Code) -### Before (Direct Usage) -```typescript -// Old way - direct component usage - -``` +For renderers that execute code (`runjs`, `runpy`): +- **Security First:** Implement a security check (`CodeSecurityService`) and a multi-click confirmation for risky code. +- **Execution Modes:** Offer a "safe mode" (sandboxed, e.g., QuickJS) and an "unsafe mode" (direct `eval`). +- **DOM Target:** Provide a `litechat.target` DOM element for the code to manipulate. This is crucial for visualizations. +- **Output Capture:** Capture `stdout`, `stderr`, and logs to display in a console view. +- **Global Manager:** Use a singleton pattern (e.g., `GlobalPythonManager`) to manage the runtime environment (Pyodide, QuickJS) across all blocks, preventing redundant loading. -### After (Universal System) -```typescript -// New way - universal renderer with registered modules - -``` +#### Parsing Complex or Unsafe Code -The Universal Block Renderer automatically selects the appropriate registered renderer based on the language and priority system. +For renderers that parse complex data structures (like `FormedibleBlockRenderer` or `FlowBlockRenderer`), implement a safe parser class. +- **Sanitize Input:** Remove comments and potentially malicious code before parsing. +- **Validate Structure:** Recursively validate the parsed object against an allowlist of keys and types. +- **Graceful Errors:** Provide specific error messages to help the user (or AI) correct the input. ## Conclusion -The Block Renderer System provides a powerful, extensible foundation for handling diverse code block types in LiteChat. By following the established patterns and best practices, you can create rich, interactive renderers that enhance the user experience while maintaining consistency with the application's architecture. \ No newline at end of file +The Block Renderer System provides a powerful, extensible foundation for handling diverse code block types in LiteChat. By following the established patterns and best practices, you can create rich, interactive renderers that enhance the user experience while maintaining consistency with the application's architecture. \ No newline at end of file diff --git a/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx b/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx index 11e80447..60b3dca3 100644 --- a/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx +++ b/src/components/LiteChat/common/OrchestrationBlockRenderer.tsx @@ -15,7 +15,7 @@ import { useSettingsStore } from "@/store/settings.store"; import { useShallow } from "zustand/react/shallow"; import { CodeBlockRenderer } from "./CodeBlockRenderer"; import type { CanvasControl } from "@/types/litechat/canvas/control"; -import { useConversationStore } from "@/store/conversation.store"; +import { useInteractionStore } from "@/store/interaction.store"; interface OrchestrationBlockProps { code: string; @@ -73,7 +73,7 @@ export const OrchestrationBlockRenderer: React.FC = ({ foldStreamingCodeBlocks: state.foldStreamingCodeBlocks, })) ); - const conversationId = useConversationStore(state => state.selectedItemId); + const conversationId = useInteractionStore(state => state.currentConversationId); const [isFolded, setIsFolded] = useState(isStreaming ? foldStreamingCodeBlocks : false); const [showCode, setShowCode] = useState(false);