diff --git a/app/api/guides/[name]/route.ts b/app/api/guides/[name]/route.ts new file mode 100644 index 0000000..224bc86 --- /dev/null +++ b/app/api/guides/[name]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { readGuide } from '@/services/guides/guideService'; + +export const runtime = 'nodejs'; + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ name: string }> } +) { + try { + const { name } = await params; + const guide = readGuide(name); + if (!guide) { + return NextResponse.json( + { success: false, error: `Guide "${name}" not found` }, + { status: 404 } + ); + } + return NextResponse.json({ success: true, data: guide }); + } catch (error) { + console.error('[API /guides/[name]] error:', error); + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Failed to read guide' }, + { status: 500 } + ); + } +} diff --git a/app/api/guides/route.ts b/app/api/guides/route.ts new file mode 100644 index 0000000..f051524 --- /dev/null +++ b/app/api/guides/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server'; +import { listGuides } from '@/services/guides/guideService'; + +export const runtime = 'nodejs'; + +export async function GET() { + try { + const guides = listGuides(); + return NextResponse.json({ success: true, data: guides }); + } catch (error) { + console.error('[API /guides] error:', error); + return NextResponse.json( + { success: false, error: error instanceof Error ? error.message : 'Failed to list guides' }, + { status: 500 } + ); + } +} diff --git a/app/api/workflows/[key]/route.ts b/app/api/workflows/[key]/route.ts deleted file mode 100644 index 141483e..0000000 --- a/app/api/workflows/[key]/route.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { saveWorkflow, deleteWorkflow, userWorkflowExists } from '@/services/workflows/workflowFileService'; -import { WorkflowRegistry, BUNDLED_WORKFLOW_KEYS } from '@/services/workflows/registry'; - -// PUT /api/workflows/[key] - Create or update a workflow -export async function PUT( - request: NextRequest, - { params }: { params: Promise<{ key: string }> } -) { - try { - const { key } = await params; - const body = await request.json(); - - // Validate required fields - if (!body.displayName || typeof body.displayName !== 'string') { - return NextResponse.json( - { success: false, error: 'displayName is required' }, - { status: 400 } - ); - } - - if (!body.instructions || typeof body.instructions !== 'string') { - return NextResponse.json( - { success: false, error: 'instructions is required' }, - { status: 400 } - ); - } - - // Save the workflow - saveWorkflow({ - key, - displayName: body.displayName.trim(), - description: body.description?.trim() || '', - instructions: body.instructions, - enabled: body.enabled !== false, - requiresFocusedNode: body.requiresFocusedNode !== false, - }); - - return NextResponse.json({ success: true }); - } catch (error) { - console.error('Error saving workflow:', error); - return NextResponse.json( - { success: false, error: 'Failed to save workflow' }, - { status: 500 } - ); - } -} - -// DELETE /api/workflows/[key] - Delete a workflow (resets to default if bundled) -export async function DELETE( - request: NextRequest, - { params }: { params: Promise<{ key: string }> } -) { - try { - const { key } = await params; - - // Check if user file exists - if (!userWorkflowExists(key)) { - // If it's a bundled workflow, nothing to delete - if (BUNDLED_WORKFLOW_KEYS.has(key)) { - return NextResponse.json( - { success: false, error: 'Cannot delete bundled workflow (no user override exists)' }, - { status: 400 } - ); - } - return NextResponse.json( - { success: false, error: 'Workflow not found' }, - { status: 404 } - ); - } - - const deleted = deleteWorkflow(key); - - if (!deleted) { - return NextResponse.json( - { success: false, error: 'Failed to delete workflow' }, - { status: 500 } - ); - } - - // If this was a bundled workflow, it will now fall back to default - const isBundled = BUNDLED_WORKFLOW_KEYS.has(key); - - return NextResponse.json({ - success: true, - resetToDefault: isBundled, - }); - } catch (error) { - console.error('Error deleting workflow:', error); - return NextResponse.json( - { success: false, error: 'Failed to delete workflow' }, - { status: 500 } - ); - } -} - -// GET /api/workflows/[key] - Get a single workflow -export async function GET( - request: NextRequest, - { params }: { params: Promise<{ key: string }> } -) { - try { - const { key } = await params; - const workflow = await WorkflowRegistry.getWorkflowByKey(key); - - if (!workflow) { - return NextResponse.json( - { success: false, error: 'Workflow not found' }, - { status: 404 } - ); - } - - // Include metadata about whether it's user-modified - const hasUserOverride = userWorkflowExists(key); - const isBundled = BUNDLED_WORKFLOW_KEYS.has(key); - - return NextResponse.json({ - success: true, - data: { - ...workflow, - isBundled, - hasUserOverride, - }, - }); - } catch (error) { - console.error('Error fetching workflow:', error); - return NextResponse.json( - { success: false, error: 'Failed to fetch workflow' }, - { status: 500 } - ); - } -} diff --git a/app/api/workflows/execute/route.ts b/app/api/workflows/execute/route.ts deleted file mode 100644 index 0bd54c3..0000000 --- a/app/api/workflows/execute/route.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { WorkflowRegistry } from '@/services/workflows/registry'; -import { getSQLiteClient } from '@/services/database/sqlite-client'; -import { WorkflowExecutor } from '@/services/agents/workflowExecutor'; -import { getAutoContextSummaries } from '@/services/context/autoContext'; - -export async function POST(request: NextRequest) { - try { - const body = await request.json(); - const { workflowKey, nodeId, userContext } = body; - - // Validate workflowKey - if (!workflowKey || typeof workflowKey !== 'string') { - return NextResponse.json( - { success: false, error: 'workflowKey is required' }, - { status: 400 } - ); - } - - // Fetch workflow definition - const workflow = await WorkflowRegistry.getWorkflowByKey(workflowKey); - if (!workflow) { - return NextResponse.json( - { success: false, error: `Workflow '${workflowKey}' not found` }, - { status: 404 } - ); - } - - if (!workflow.enabled) { - return NextResponse.json( - { success: false, error: `Workflow '${workflowKey}' is disabled` }, - { status: 400 } - ); - } - - // Validate node requirement - if (workflow.requiresFocusedNode && !nodeId) { - return NextResponse.json( - { success: false, error: `Workflow '${workflowKey}' requires a nodeId` }, - { status: 400 } - ); - } - - // Prevent re-running same workflow on same node within 1 hour - if (nodeId) { - const db = getSQLiteClient(); - const recentRuns = db.query<{ id: number }>( - `SELECT id FROM chats - WHERE json_extract(metadata, '$.workflow_key') = ? - AND json_extract(metadata, '$.workflow_node_id') = ? - AND datetime(created_at) > datetime('now', '-1 hour') - LIMIT 1`, - [workflowKey, nodeId] - ).rows; - - if (recentRuns.length > 0) { - return NextResponse.json( - { - success: false, - error: `Workflow '${workflowKey}' already ran on node ${nodeId} within the last hour` - }, - { status: 429 } - ); - } - } - - // Build context - let contextLines: string[] = []; - if (nodeId) { - const db = getSQLiteClient(); - const stmt = db.prepare('SELECT * FROM nodes WHERE id = ?'); - const node = stmt.get(nodeId) as any; - if (!node) { - return NextResponse.json( - { success: false, error: `Node ${nodeId} not found` }, - { status: 404 } - ); - } - - contextLines = [ - `Focused Node: [NODE:${node.id}:"${node.title}"]`, - node.description ? `Description: ${node.description}` : null, - node.content ? `Content: ${node.content}` : null, - node.link ? `Link: ${node.link}` : null, - ].filter(Boolean) as string[]; - } - - if (userContext) { - contextLines.push(`User Context: ${userContext}`); - } - - // Add auto-context summaries - const autoContextSummaries = getAutoContextSummaries(6); - if (autoContextSummaries.length > 0) { - contextLines.push('Background Context (Top Hubs):'); - autoContextSummaries.forEach((summary) => { - contextLines.push( - `[NODE:${summary.id}:"${summary.title}"] (${summary.edgeCount} edges)` - ); - }); - } - - const task = `Execute workflow: ${workflow.displayName} - -${workflow.instructions} - -${nodeId ? `Target Node ID: ${nodeId}` : 'No specific node targeted (general workflow)'}`; - - const sessionId = `workflow_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; - - try { - // Execute workflow synchronously - const result = await WorkflowExecutor.execute({ - sessionId, - task, - context: contextLines, - expectedOutcome: workflow.expectedOutcome, - workflowKey, - workflowNodeId: nodeId, - }); - - return NextResponse.json({ - success: true, - sessionId, - workflowKey, - nodeId: nodeId || null, - status: 'completed', - summary: result.summary, - message: `Workflow '${workflow.displayName}' completed`, - }); - } catch (error) { - console.error('[/api/workflows/execute] Execution failed:', error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return NextResponse.json( - { success: false, error: `Workflow execution failed: ${errorMessage}` }, - { status: 500 } - ); - } - } catch (error) { - console.error('Error executing workflow:', error); - return NextResponse.json( - { success: false, error: 'Failed to execute workflow' }, - { status: 500 } - ); - } -} diff --git a/app/api/workflows/route.ts b/app/api/workflows/route.ts deleted file mode 100644 index 056b6bd..0000000 --- a/app/api/workflows/route.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NextResponse } from 'next/server'; -import { WorkflowRegistry } from '@/services/workflows/registry'; - -export async function GET() { - try { - const workflows = await WorkflowRegistry.getAllWorkflows(); - return NextResponse.json({ - success: true, - data: workflows, - }); - } catch (error) { - console.error('Error fetching workflows:', error); - return NextResponse.json( - { success: false, error: 'Failed to fetch workflows' }, - { status: 500 } - ); - } -} diff --git a/package-lock.json b/package-lock.json index 0574fca..f9402fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "ai": "^6.0.27", "better-sqlite3": "^12.2.0", "cheerio": "^1.1.2", + "gray-matter": "^4.0.3", "lucide-react": "^0.468.0", "next": "15.1.3", "openai": "^4.103.0", @@ -5056,6 +5057,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -5171,6 +5185,18 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5607,6 +5633,43 @@ "dev": true, "license": "MIT" }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -6100,6 +6163,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6526,6 +6598,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/langsmith": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/langsmith/-/langsmith-0.4.5.tgz", @@ -9131,6 +9212,19 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -9418,6 +9512,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -9620,6 +9720,15 @@ "node": ">=4" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/package.json b/package.json index 1fc3d7f..7f5d2a9 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "ai": "^6.0.27", "better-sqlite3": "^12.2.0", "cheerio": "^1.1.2", + "gray-matter": "^4.0.3", "lucide-react": "^0.468.0", "next": "15.1.3", "openai": "^4.103.0", diff --git a/src/components/layout/LeftToolbar.tsx b/src/components/layout/LeftToolbar.tsx index ec852e5..09c288e 100644 --- a/src/components/layout/LeftToolbar.tsx +++ b/src/components/layout/LeftToolbar.tsx @@ -7,7 +7,7 @@ import { LayoutList, Map, Folder, - Workflow, + FileText, Settings, } from 'lucide-react'; import type { PaneType } from '../panes/types'; @@ -27,18 +27,18 @@ const PANE_TYPE_ICONS: Record = { views: LayoutList, map: Map, dimensions: Folder, - workflows: Workflow, + guides: FileText, }; const PANE_TYPE_LABELS: Record = { views: 'Feed', map: 'Map', dimensions: 'Dimensions', - workflows: 'Workflows', + guides: 'Guides', }; // Pane types shown in the toolbar (excludes 'node' which is opened via Feed, chat removed in rah-light) -const TOOLBAR_PANE_TYPES: PaneType[] = ['views', 'map', 'dimensions', 'workflows']; +const TOOLBAR_PANE_TYPES: PaneType[] = ['views', 'map', 'dimensions', 'guides']; interface ToolbarButtonProps { icon: typeof Search; diff --git a/src/components/layout/ThreePanelLayout.tsx b/src/components/layout/ThreePanelLayout.tsx index d5a9ff5..5c468a8 100644 --- a/src/components/layout/ThreePanelLayout.tsx +++ b/src/components/layout/ThreePanelLayout.tsx @@ -26,7 +26,7 @@ import LeftToolbar from './LeftToolbar'; import SplitHandle from './SplitHandle'; // Pane components (ChatPane removed in rah-light) -import { NodePane, WorkflowsPane, DimensionsPane, MapPane, ViewsPane } from '../panes'; +import { NodePane, GuidesPane, DimensionsPane, MapPane, ViewsPane } from '../panes'; import QuickAddInput from '../agents/QuickAddInput'; import type { PaneType, SlotState, PaneAction } from '../panes/types'; @@ -258,9 +258,9 @@ export default function ThreePanelLayout() { // Delegation events ignored (delegation system removed in rah-light) break; - case 'WORKFLOW_PROGRESS': + case 'GUIDE_UPDATED': if (typeof window !== 'undefined') { - window.dispatchEvent(new CustomEvent('workflow:progress', { detail: data.data })); + window.dispatchEvent(new CustomEvent('guides:updated', { detail: data.data })); } break; @@ -807,22 +807,14 @@ export default function ThreePanelLayout() { // case 'chat' removed in rah-light - case 'workflows': + case 'guides': return ( - { - handleNodeSelect(nodeId, false); - setActivePane(slot); - }} - openTabsData={openTabsData} - activeTabId={activeTab} - activeDimension={activeDimension} /> ); diff --git a/src/components/panes/GuidesPane.tsx b/src/components/panes/GuidesPane.tsx new file mode 100644 index 0000000..916396f --- /dev/null +++ b/src/components/panes/GuidesPane.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { ArrowLeft } from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import PaneHeader from './PaneHeader'; +import type { BasePaneProps } from './types'; + +interface GuideMeta { + name: string; + description: string; +} + +interface Guide extends GuideMeta { + content: string; +} + +export default function GuidesPane({ + slot, + isActive, + onPaneAction, + onCollapse, + onSwapPanes, + tabBar, +}: BasePaneProps) { + const [guides, setGuides] = useState([]); + const [selectedGuide, setSelectedGuide] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchGuides(); + + const handleGuideUpdated = () => { fetchGuides(); }; + window.addEventListener('guides:updated', handleGuideUpdated); + return () => window.removeEventListener('guides:updated', handleGuideUpdated); + }, []); + + const fetchGuides = async () => { + try { + const res = await fetch('/api/guides'); + const data = await res.json(); + if (data.success) { + setGuides(data.data); + } + } catch (err) { + console.error('[GuidesPane] Failed to fetch guides:', err); + } finally { + setLoading(false); + } + }; + + const handleSelectGuide = async (name: string) => { + try { + const res = await fetch(`/api/guides/${encodeURIComponent(name)}`); + const data = await res.json(); + if (data.success) { + setSelectedGuide(data.data); + } + } catch (err) { + console.error('[GuidesPane] Failed to fetch guide:', err); + } + }; + + const handleBack = () => { + setSelectedGuide(null); + }; + + return ( +
+ + {selectedGuide && ( +
+ + + {selectedGuide.name} + +
+ )} +
+ +
+ {loading ? ( +
+ Loading... +
+ ) : selectedGuide ? ( +
+ ( +

{children}

+ ), + h2: ({ children }) => ( +

{children}

+ ), + h3: ({ children }) => ( +

{children}

+ ), + p: ({ children }) => ( +

{children}

+ ), + ul: ({ children }) => ( +
    {children}
+ ), + ol: ({ children }) => ( +
    {children}
+ ), + li: ({ children }) => ( +
  • {children}
  • + ), + code: ({ className, children, ...props }) => { + const isInline = !className; + if (isInline) { + return ( + {children} + ); + } + return ( + {children} + ); + }, + pre: ({ children }) => ( +
    {children}
    + ), + strong: ({ children }) => ( + {children} + ), + hr: () => ( +
    + ), + blockquote: ({ children }) => ( +
    {children}
    + ), + }} + > + {selectedGuide.content} +
    +
    + ) : ( +
    + {guides.length === 0 ? ( +
    + No guides found +
    + ) : ( + guides.map((guide) => ( + + )) + )} +
    + )} +
    +
    + ); +} diff --git a/src/components/panes/WorkflowsPane.tsx b/src/components/panes/WorkflowsPane.tsx deleted file mode 100644 index da0734a..0000000 --- a/src/components/panes/WorkflowsPane.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client"; - -import { useState } from 'react'; -import PaneHeader from './PaneHeader'; -import { WorkflowsPaneProps, AgentDelegation } from './types'; - -export default function WorkflowsPane({ - slot, - isActive, - onPaneAction, - onCollapse, - onSwapPanes, - delegations, - onNodeClick, - openTabsData = [], - activeTabId = null, - activeDimension, -}: WorkflowsPaneProps) { - return ( -
    - - -
    - -
    -
    - ); -} - -// Workflows list view - simplified for rah-light (delegation system removed) -function WorkflowsListView() { - return ( -
    - {/* Header */} -
    - - Workflows - -
    - - {/* Content */} -
    -
    -

    - Workflows are available via MCP server integration. -

    -

    - Connect your coding agent (Claude Code, Cursor, etc.) via MCP to run workflows like Integrate, Extract, and more. -

    -
    -
    -
    - ); -} diff --git a/src/components/panes/index.ts b/src/components/panes/index.ts index 1c24365..ed204ed 100644 --- a/src/components/panes/index.ts +++ b/src/components/panes/index.ts @@ -1,5 +1,5 @@ export { default as NodePane } from './NodePane'; -export { default as WorkflowsPane } from './WorkflowsPane'; +export { default as GuidesPane } from './GuidesPane'; export { default as DimensionsPane } from './DimensionsPane'; export { default as MapPane } from './MapPane'; export { default as ViewsPane } from './ViewsPane'; diff --git a/src/components/panes/types.ts b/src/components/panes/types.ts index 8461676..195d9dc 100644 --- a/src/components/panes/types.ts +++ b/src/components/panes/types.ts @@ -15,7 +15,7 @@ export type AgentDelegation = { }; // The five pane types (chat removed in rah-light) -export type PaneType = 'node' | 'workflows' | 'dimensions' | 'map' | 'views'; +export type PaneType = 'node' | 'guides' | 'dimensions' | 'map' | 'views'; // State for each slot export interface SlotState { @@ -68,14 +68,7 @@ export interface HighlightedPassage { // ChatPaneProps removed in rah-light -// WorkflowsPane specific props -export interface WorkflowsPaneProps extends BasePaneProps { - delegations: AgentDelegation[]; - onNodeClick?: (nodeId: number) => void; - openTabsData?: Node[]; - activeTabId?: number | null; - activeDimension?: string | null; -} +// GuidesPaneProps - just uses BasePaneProps (guides are self-contained) // DimensionsPane specific props export interface DimensionsPaneProps extends BasePaneProps { @@ -110,7 +103,7 @@ export interface PaneHeaderProps { // Labels for pane types export const PANE_LABELS: Record = { node: 'Nodes', - workflows: 'Workflows', + guides: 'Guides', dimensions: 'Dimensions', map: 'Map', views: 'Feed', @@ -124,5 +117,5 @@ export const DEFAULT_SLOT_A: SlotState = { }; export const DEFAULT_SLOT_B: SlotState = { - type: 'workflows', + type: 'guides', }; diff --git a/src/components/settings/ContextViewer.tsx b/src/components/settings/ContextViewer.tsx index 23c89cc..eb47786 100644 --- a/src/components/settings/ContextViewer.tsx +++ b/src/components/settings/ContextViewer.tsx @@ -73,7 +73,7 @@ export default function ContextViewer() { return (

    - Top 10 most-connected nodes are added to background context for workflow execution. + Top 10 most-connected nodes are added to background context for tool execution.

    {/* Toggle */} diff --git a/src/components/settings/GuidesViewer.tsx b/src/components/settings/GuidesViewer.tsx new file mode 100644 index 0000000..3567fad --- /dev/null +++ b/src/components/settings/GuidesViewer.tsx @@ -0,0 +1,306 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { Plus, Pencil, Trash2, Save, X, FileText } from 'lucide-react'; + +interface GuideMeta { + name: string; + description: string; +} + +interface Guide extends GuideMeta { + content: string; +} + +export default function GuidesViewer() { + const [guides, setGuides] = useState([]); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(null); + const [isNew, setIsNew] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + fetchGuides(); + }, []); + + const fetchGuides = async () => { + try { + const res = await fetch('/api/guides'); + const data = await res.json(); + if (data.success) { + setGuides(data.data); + } + } catch (err) { + console.error('Failed to fetch guides:', err); + } finally { + setLoading(false); + } + }; + + const handleEdit = async (name: string) => { + try { + const res = await fetch(`/api/guides/${encodeURIComponent(name)}`); + const data = await res.json(); + if (data.success) { + setEditing(data.data); + setIsNew(false); + setError(null); + } + } catch (err) { + console.error('Failed to fetch guide:', err); + } + }; + + const handleNew = () => { + setEditing({ + name: '', + description: '', + content: '# New Guide\n\nWrite your guide content here...', + }); + setIsNew(true); + setError(null); + }; + + const handleSave = async () => { + if (!editing) return; + if (!editing.name.trim()) { + setError('Name is required'); + return; + } + + try { + const res = await fetch(`/api/guides/${encodeURIComponent(editing.name)}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + content: editing.content, + description: editing.description, + }), + }); + const data = await res.json(); + if (data.success) { + setEditing(null); + fetchGuides(); + window.dispatchEvent(new Event('guides:updated')); + } else { + setError(data.error || 'Failed to save'); + } + } catch (err) { + setError('Failed to save guide'); + } + }; + + const handleDelete = async (name: string) => { + if (!confirm(`Delete guide "${name}"?`)) return; + + try { + const res = await fetch(`/api/guides/${encodeURIComponent(name)}`, { + method: 'DELETE', + }); + const data = await res.json(); + if (data.success) { + fetchGuides(); + window.dispatchEvent(new Event('guides:updated')); + } + } catch (err) { + console.error('Failed to delete guide:', err); + } + }; + + if (loading) { + return ( +
    Loading guides...
    + ); + } + + if (editing) { + return ( +
    +
    + setEditing({ ...editing, name: e.target.value })} + placeholder="Guide name" + disabled={!isNew} + style={{ + flex: 1, + padding: '8px 12px', + background: '#1a1a1a', + border: '1px solid #333', + borderRadius: '6px', + color: '#fff', + fontSize: '14px', + }} + /> + + +
    + + {error && ( +
    {error}
    + )} + + setEditing({ ...editing, description: e.target.value })} + placeholder="Brief description" + style={{ + padding: '8px 12px', + background: '#1a1a1a', + border: '1px solid #333', + borderRadius: '6px', + color: '#fff', + fontSize: '13px', + marginBottom: '16px', + }} + /> + +