feat(rah-light): replace workflows with guides system
- Remove entire workflow system (API routes, services, tools, components) - Add guides system from main ra-h repo (for external agent integration) - Update pane types: 'workflows' → 'guides' throughout - Update LeftToolbar to show guides icon instead of workflows - Update SettingsModal with GuidesViewer tab - Add GuidesPane for browsing guides in main UI - Clean up prompts to remove workflow references - Update tool registry to remove workflow tools - Add gray-matter package for frontmatter parsing Guides are essential for RA-H Light as they help external agents (via MCP) understand the knowledge base context and usage patterns. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
f383d770ca
commit
b31441b35f
@@ -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<string, typeof LayoutList> = {
|
||||
views: LayoutList,
|
||||
map: Map,
|
||||
dimensions: Folder,
|
||||
workflows: Workflow,
|
||||
guides: FileText,
|
||||
};
|
||||
|
||||
const PANE_TYPE_LABELS: Record<string, string> = {
|
||||
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;
|
||||
|
||||
@@ -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 (
|
||||
<WorkflowsPane
|
||||
<GuidesPane
|
||||
slot={slot}
|
||||
isActive={isActive}
|
||||
onPaneAction={slot === 'A' ? handleSlotAAction : handleSlotBAction}
|
||||
onCollapse={onCollapse}
|
||||
onSwapPanes={slotB ? handleSwapPanes : undefined}
|
||||
delegations={delegations}
|
||||
onNodeClick={(nodeId) => {
|
||||
handleNodeSelect(nodeId, false);
|
||||
setActivePane(slot);
|
||||
}}
|
||||
openTabsData={openTabsData}
|
||||
activeTabId={activeTab}
|
||||
activeDimension={activeDimension}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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<GuideMeta[]>([]);
|
||||
const [selectedGuide, setSelectedGuide] = useState<Guide | null>(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 (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'transparent',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<PaneHeader slot={slot} onCollapse={onCollapse} onSwapPanes={onSwapPanes} tabBar={tabBar}>
|
||||
{selectedGuide && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<button
|
||||
onClick={handleBack}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '4px',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
onMouseEnter={e => { e.currentTarget.style.color = '#ccc'; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.color = '#888'; }}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<span style={{ color: '#ccc', fontSize: '13px', fontWeight: 500 }}>
|
||||
{selectedGuide.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</PaneHeader>
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: 'auto', padding: '12px' }}>
|
||||
{loading ? (
|
||||
<div style={{ color: '#555', fontSize: '13px', textAlign: 'center', paddingTop: '24px' }}>
|
||||
Loading...
|
||||
</div>
|
||||
) : selectedGuide ? (
|
||||
<div className="guide-content" style={{ color: '#ccc', fontSize: '13px', lineHeight: '1.6' }}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ children }) => (
|
||||
<h1 style={{ fontSize: '18px', fontWeight: 600, color: '#eee', margin: '0 0 16px 0' }}>{children}</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 style={{ fontSize: '15px', fontWeight: 600, color: '#ddd', margin: '20px 0 8px 0' }}>{children}</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 style={{ fontSize: '14px', fontWeight: 600, color: '#ccc', margin: '16px 0 6px 0' }}>{children}</h3>
|
||||
),
|
||||
p: ({ children }) => (
|
||||
<p style={{ margin: '0 0 12px 0' }}>{children}</p>
|
||||
),
|
||||
ul: ({ children }) => (
|
||||
<ul style={{ margin: '0 0 12px 0', paddingLeft: '20px' }}>{children}</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol style={{ margin: '0 0 12px 0', paddingLeft: '20px' }}>{children}</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li style={{ margin: '0 0 4px 0' }}>{children}</li>
|
||||
),
|
||||
code: ({ className, children, ...props }) => {
|
||||
const isInline = !className;
|
||||
if (isInline) {
|
||||
return (
|
||||
<code style={{
|
||||
background: '#1a1a1a',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
color: '#22c55e',
|
||||
}} {...props}>{children}</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code style={{
|
||||
display: 'block',
|
||||
background: '#0d0d0d',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
overflowX: 'auto',
|
||||
margin: '0 0 12px 0',
|
||||
color: '#aaa',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}} {...props}>{children}</code>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre style={{ margin: '0 0 12px 0' }}>{children}</pre>
|
||||
),
|
||||
strong: ({ children }) => (
|
||||
<strong style={{ color: '#eee', fontWeight: 600 }}>{children}</strong>
|
||||
),
|
||||
hr: () => (
|
||||
<hr style={{ border: 'none', borderTop: '1px solid #2a2a2a', margin: '16px 0' }} />
|
||||
),
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote style={{
|
||||
borderLeft: '3px solid #333',
|
||||
paddingLeft: '12px',
|
||||
margin: '0 0 12px 0',
|
||||
color: '#999',
|
||||
}}>{children}</blockquote>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{selectedGuide.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{guides.length === 0 ? (
|
||||
<div style={{ color: '#555', fontSize: '13px', textAlign: 'center', paddingTop: '24px' }}>
|
||||
No guides found
|
||||
</div>
|
||||
) : (
|
||||
guides.map((guide) => (
|
||||
<button
|
||||
key={guide.name}
|
||||
onClick={() => handleSelectGuide(guide.name)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '4px',
|
||||
padding: '12px',
|
||||
background: '#161616',
|
||||
border: '1px solid #222',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
e.currentTarget.style.borderColor = '#333';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.background = '#161616';
|
||||
e.currentTarget.style.borderColor = '#222';
|
||||
}}
|
||||
>
|
||||
<span style={{ color: '#ddd', fontSize: '13px', fontWeight: 500 }}>
|
||||
{guide.name}
|
||||
</span>
|
||||
<span style={{ color: '#777', fontSize: '12px', lineHeight: '1.4' }}>
|
||||
{guide.description}
|
||||
</span>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'transparent',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<PaneHeader slot={slot} onCollapse={onCollapse} onSwapPanes={onSwapPanes} />
|
||||
|
||||
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
||||
<WorkflowsListView />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Workflows list view - simplified for rah-light (delegation system removed)
|
||||
function WorkflowsListView() {
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: 'transparent',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
padding: '16px 20px',
|
||||
borderBottom: '1px solid #1a1a1a',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<span style={{
|
||||
color: '#e5e5e5',
|
||||
fontSize: '13px',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em'
|
||||
}}>
|
||||
Workflows
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
padding: '12px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '16px'
|
||||
}}>
|
||||
<div style={{
|
||||
color: '#666',
|
||||
fontSize: '12px',
|
||||
textAlign: 'center',
|
||||
padding: '40px 20px',
|
||||
maxWidth: '300px'
|
||||
}}>
|
||||
<p style={{ marginBottom: '16px' }}>
|
||||
Workflows are available via MCP server integration.
|
||||
</p>
|
||||
<p style={{ color: '#555', fontSize: '11px' }}>
|
||||
Connect your coding agent (Claude Code, Cursor, etc.) via MCP to run workflows like Integrate, Extract, and more.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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<PaneType, string> = {
|
||||
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',
|
||||
};
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function ContextViewer() {
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<p style={descStyle}>
|
||||
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.
|
||||
</p>
|
||||
|
||||
{/* Toggle */}
|
||||
|
||||
@@ -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<GuideMeta[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editing, setEditing] = useState<Guide | null>(null);
|
||||
const [isNew, setIsNew] = useState(false);
|
||||
const [error, setError] = useState<string | null>(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 (
|
||||
<div style={{ padding: '24px', color: '#666' }}>Loading guides...</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: '24px' }}>
|
||||
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={editing.name}
|
||||
onChange={(e) => 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',
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#22c55e',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#000',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Save size={14} /> Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditing(null)}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: 'transparent',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '6px',
|
||||
color: '#888',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
<X size={14} /> Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{ color: '#ef4444', fontSize: '13px', marginBottom: '12px' }}>{error}</div>
|
||||
)}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={editing.description}
|
||||
onChange={(e) => 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',
|
||||
}}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
value={editing.content}
|
||||
onChange={(e) => setEditing({ ...editing, content: e.target.value })}
|
||||
placeholder="Guide content (markdown)"
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px',
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '6px',
|
||||
color: '#ccc',
|
||||
fontSize: '13px',
|
||||
fontFamily: 'monospace',
|
||||
resize: 'none',
|
||||
lineHeight: 1.5,
|
||||
}}
|
||||
/>
|
||||
|
||||
<p style={{ color: '#666', fontSize: '12px', marginTop: '12px' }}>
|
||||
Guides are markdown files that external agents can read via MCP tools. Use them to provide context, instructions, or reference material.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: '24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<p style={{ color: '#888', fontSize: '13px', margin: 0 }}>
|
||||
Guides provide context and instructions for external AI agents via MCP.
|
||||
</p>
|
||||
<button
|
||||
onClick={handleNew}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
background: '#22c55e',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#000',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Plus size={14} /> New Guide
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
{guides.length === 0 ? (
|
||||
<div style={{ color: '#555', textAlign: 'center', paddingTop: '48px' }}>
|
||||
<FileText size={48} style={{ marginBottom: '12px', opacity: 0.5 }} />
|
||||
<p style={{ fontSize: '14px' }}>No guides yet</p>
|
||||
<p style={{ fontSize: '12px', color: '#444' }}>Create guides to help external agents understand your knowledge base</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{guides.map((guide) => (
|
||||
<div
|
||||
key={guide.name}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
padding: '12px 16px',
|
||||
background: '#161616',
|
||||
border: '1px solid #222',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<FileText size={18} style={{ color: '#22c55e', flexShrink: 0 }} />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ color: '#ddd', fontSize: '14px', fontWeight: 500 }}>{guide.name}</div>
|
||||
<div style={{ color: '#666', fontSize: '12px', marginTop: '2px' }}>{guide.description}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleEdit(guide.name)}
|
||||
style={{
|
||||
padding: '6px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(guide.name)}
|
||||
style={{
|
||||
padding: '6px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,17 +4,17 @@ import { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import LogsViewer from './LogsViewer';
|
||||
import ToolsViewer from './ToolsViewer';
|
||||
import WorkflowsViewer from './WorkflowsViewer';
|
||||
import ApiKeysViewer from './ApiKeysViewer';
|
||||
import DatabaseViewer from './DatabaseViewer';
|
||||
import ExternalAgentsPanel from './ExternalAgentsPanel';
|
||||
import ContextViewer from './ContextViewer';
|
||||
import GuidesViewer from './GuidesViewer';
|
||||
import { apiKeyService } from '@/services/storage/apiKeys';
|
||||
|
||||
export type SettingsTab =
|
||||
| 'logs'
|
||||
| 'tools'
|
||||
| 'workflows'
|
||||
| 'guides'
|
||||
| 'apikeys'
|
||||
| 'database'
|
||||
| 'context'
|
||||
@@ -140,18 +140,18 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
|
||||
Tools
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setActiveTab('workflows')}
|
||||
onClick={() => setActiveTab('guides')}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
fontSize: '14px',
|
||||
color: activeTab === 'workflows' ? '#fff' : '#888',
|
||||
background: activeTab === 'workflows' ? '#1a3a2a' : 'transparent',
|
||||
borderLeft: activeTab === 'workflows' ? '3px solid #22c55e' : '3px solid transparent',
|
||||
color: activeTab === 'guides' ? '#fff' : '#888',
|
||||
background: activeTab === 'guides' ? '#1a3a2a' : 'transparent',
|
||||
borderLeft: activeTab === 'guides' ? '3px solid #22c55e' : '3px solid transparent',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
>
|
||||
Workflows
|
||||
Guides
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setActiveTab('apikeys')}
|
||||
@@ -295,7 +295,7 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
|
||||
>
|
||||
{activeTab === 'logs' && 'System Logs'}
|
||||
{activeTab === 'tools' && 'Tools'}
|
||||
{activeTab === 'workflows' && 'Workflows'}
|
||||
{activeTab === 'guides' && 'Guides'}
|
||||
{activeTab === 'apikeys' && 'API Keys'}
|
||||
{activeTab === 'database' && 'Knowledge Database'}
|
||||
{activeTab === 'context' && 'Auto-Context'}
|
||||
@@ -329,7 +329,7 @@ export default function SettingsModal({ isOpen, onClose, initialTab }: SettingsM
|
||||
<div style={{ flex: 1, overflow: 'hidden' }}>
|
||||
{activeTab === 'logs' && <LogsViewer key={isOpen ? 'open' : 'closed'} />}
|
||||
{activeTab === 'tools' && <ToolsViewer />}
|
||||
{activeTab === 'workflows' && <WorkflowsViewer />}
|
||||
{activeTab === 'guides' && <GuidesViewer />}
|
||||
{activeTab === 'apikeys' && <ApiKeysViewer />}
|
||||
{activeTab === 'database' && <DatabaseViewer />}
|
||||
{activeTab === 'context' && <ContextViewer />}
|
||||
|
||||
@@ -1,464 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, type CSSProperties } from 'react';
|
||||
|
||||
interface WorkflowDefinition {
|
||||
id: number;
|
||||
key: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
enabled: boolean;
|
||||
requiresFocusedNode: boolean;
|
||||
primaryActor: 'oracle' | 'main';
|
||||
expectedOutcome?: string;
|
||||
isBundled?: boolean;
|
||||
hasUserOverride?: boolean;
|
||||
}
|
||||
|
||||
interface EditingWorkflow {
|
||||
key: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
instructions: string;
|
||||
enabled: boolean;
|
||||
requiresFocusedNode: boolean;
|
||||
isNew: boolean;
|
||||
}
|
||||
|
||||
export default function WorkflowsViewer() {
|
||||
const [workflows, setWorkflows] = useState<WorkflowDefinition[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editing, setEditing] = useState<EditingWorkflow | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadWorkflows = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/workflows');
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
// Fetch additional metadata for each workflow
|
||||
const enriched = await Promise.all(
|
||||
result.data.map(async (w: WorkflowDefinition) => {
|
||||
try {
|
||||
const detailRes = await fetch(`/api/workflows/${w.key}`);
|
||||
const detail = await detailRes.json();
|
||||
return detail.success ? detail.data : w;
|
||||
} catch {
|
||||
return w;
|
||||
}
|
||||
})
|
||||
);
|
||||
setWorkflows(enriched);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load workflows:', e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkflows();
|
||||
}, []);
|
||||
|
||||
const handleEdit = (workflow: WorkflowDefinition) => {
|
||||
setEditing({
|
||||
key: workflow.key,
|
||||
displayName: workflow.displayName,
|
||||
description: workflow.description,
|
||||
instructions: workflow.instructions,
|
||||
enabled: workflow.enabled,
|
||||
requiresFocusedNode: workflow.requiresFocusedNode,
|
||||
isNew: false,
|
||||
});
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleNewWorkflow = () => {
|
||||
setEditing({
|
||||
key: '',
|
||||
displayName: '',
|
||||
description: '',
|
||||
instructions: '',
|
||||
enabled: true,
|
||||
requiresFocusedNode: true,
|
||||
isNew: true,
|
||||
});
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditing(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const generateKey = (name: string): string => {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editing) return;
|
||||
|
||||
const key = editing.isNew ? generateKey(editing.displayName) : editing.key;
|
||||
|
||||
if (!key) {
|
||||
setError('Please enter a workflow name');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!editing.instructions.trim()) {
|
||||
setError('Please enter instructions');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/workflows/${key}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
displayName: editing.displayName,
|
||||
description: editing.description,
|
||||
instructions: editing.instructions,
|
||||
enabled: editing.enabled,
|
||||
requiresFocusedNode: editing.requiresFocusedNode,
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (result.success) {
|
||||
setEditing(null);
|
||||
await loadWorkflows();
|
||||
} else {
|
||||
setError(result.error || 'Failed to save workflow');
|
||||
}
|
||||
} catch (e) {
|
||||
setError('Failed to save workflow');
|
||||
console.error('Save error:', e);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (workflow: WorkflowDefinition) => {
|
||||
const action = workflow.isBundled ? 'reset to default' : 'delete';
|
||||
if (!confirm(`Are you sure you want to ${action} "${workflow.displayName}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/workflows/${workflow.key}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
|
||||
if (result.success) {
|
||||
await loadWorkflows();
|
||||
} else {
|
||||
alert(result.error || 'Failed to delete workflow');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Failed to delete workflow');
|
||||
console.error('Delete error:', e);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div style={loadingStyle}>Loading...</div>;
|
||||
}
|
||||
|
||||
// Editing view
|
||||
if (editing) {
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<div style={headerStyle}>
|
||||
<span style={{ fontSize: 14, fontWeight: 500 }}>
|
||||
{editing.isNew ? 'New Workflow' : `Edit: ${editing.displayName}`}
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button onClick={handleCancel} style={buttonStyle} disabled={saving}>
|
||||
Cancel
|
||||
</button>
|
||||
<button onClick={handleSave} style={primaryButtonStyle} disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div style={errorStyle}>{error}</div>}
|
||||
|
||||
<div style={formStyle}>
|
||||
<div style={fieldStyle}>
|
||||
<label style={labelStyle}>Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editing.displayName}
|
||||
onChange={(e) => setEditing({ ...editing, displayName: e.target.value })}
|
||||
placeholder="My Workflow"
|
||||
style={inputStyle}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={fieldStyle}>
|
||||
<label style={labelStyle}>Description (shown to agent)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editing.description}
|
||||
onChange={(e) => setEditing({ ...editing, description: e.target.value })}
|
||||
placeholder="Brief description of what this workflow does"
|
||||
style={inputStyle}
|
||||
disabled={saving}
|
||||
/>
|
||||
<span style={hintStyle}>Keep this short — it tells the agent when to use this workflow</span>
|
||||
</div>
|
||||
|
||||
<div style={fieldStyle}>
|
||||
<label style={labelStyle}>Instructions</label>
|
||||
<textarea
|
||||
value={editing.instructions}
|
||||
onChange={(e) => setEditing({ ...editing, instructions: e.target.value })}
|
||||
placeholder="Enter the workflow instructions..."
|
||||
style={textareaStyle}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={checkboxRowStyle}>
|
||||
<label style={checkboxLabelStyle}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editing.enabled}
|
||||
onChange={(e) => setEditing({ ...editing, enabled: e.target.checked })}
|
||||
disabled={saving}
|
||||
/>
|
||||
Enabled
|
||||
</label>
|
||||
<label style={checkboxLabelStyle}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editing.requiresFocusedNode}
|
||||
onChange={(e) => setEditing({ ...editing, requiresFocusedNode: e.target.checked })}
|
||||
disabled={saving}
|
||||
/>
|
||||
Requires focused node
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// List view
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<div style={headerStyle}>
|
||||
<p style={descStyle}>Workflows available to the agent.</p>
|
||||
<button onClick={handleNewWorkflow} style={primaryButtonStyle}>
|
||||
+ New Workflow
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{workflows.length === 0 ? (
|
||||
<div style={emptyStyle}>No workflows defined.</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{workflows.map((w) => (
|
||||
<div key={w.key} style={cardStyle}>
|
||||
<div style={cardContentStyle}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
||||
<span style={titleStyle}>{w.displayName}</span>
|
||||
<span style={keyStyle}>{w.key}</span>
|
||||
{w.hasUserOverride && w.isBundled && (
|
||||
<span style={modifiedBadgeStyle}>modified</span>
|
||||
)}
|
||||
{!w.enabled && (
|
||||
<span style={disabledBadgeStyle}>disabled</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={descRowStyle}>{w.description}</div>
|
||||
</div>
|
||||
<div style={actionsStyle}>
|
||||
<button onClick={() => handleEdit(w)} style={buttonStyle}>
|
||||
Edit
|
||||
</button>
|
||||
{/* Show delete for user-created, or reset for modified bundled */}
|
||||
{(!w.isBundled || w.hasUserOverride) && (
|
||||
<button
|
||||
onClick={() => handleDelete(w)}
|
||||
style={w.isBundled ? resetButtonStyle : deleteButtonStyle}
|
||||
>
|
||||
{w.isBundled ? 'Reset' : 'Delete'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Styles
|
||||
const containerStyle: CSSProperties = { padding: 24, height: '100%', overflow: 'auto' };
|
||||
const loadingStyle: CSSProperties = { padding: 24, color: '#6b7280' };
|
||||
const descStyle: CSSProperties = { fontSize: 13, color: '#6b7280', margin: 0 };
|
||||
const emptyStyle: CSSProperties = { fontSize: 13, color: '#6b7280', textAlign: 'center', padding: 32 };
|
||||
|
||||
const headerStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
};
|
||||
|
||||
const cardStyle: CSSProperties = {
|
||||
background: 'rgba(255, 255, 255, 0.02)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.06)',
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden',
|
||||
};
|
||||
|
||||
const cardContentStyle: CSSProperties = {
|
||||
padding: '14px 16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
gap: 16,
|
||||
};
|
||||
|
||||
const titleStyle: CSSProperties = { fontSize: 13, fontWeight: 500, color: '#e5e7eb' };
|
||||
const keyStyle: CSSProperties = { fontSize: 11, fontFamily: 'monospace', color: '#6b7280' };
|
||||
const descRowStyle: CSSProperties = { fontSize: 12, color: '#9ca3af', lineHeight: 1.5 };
|
||||
|
||||
const actionsStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const buttonStyle: CSSProperties = {
|
||||
padding: '6px 12px',
|
||||
fontSize: 12,
|
||||
background: 'rgba(255, 255, 255, 0.05)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 6,
|
||||
color: '#e5e7eb',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const primaryButtonStyle: CSSProperties = {
|
||||
...buttonStyle,
|
||||
background: 'rgba(34, 197, 94, 0.2)',
|
||||
borderColor: 'rgba(34, 197, 94, 0.3)',
|
||||
color: '#22c55e',
|
||||
};
|
||||
|
||||
const deleteButtonStyle: CSSProperties = {
|
||||
...buttonStyle,
|
||||
background: 'rgba(239, 68, 68, 0.1)',
|
||||
borderColor: 'rgba(239, 68, 68, 0.2)',
|
||||
color: '#ef4444',
|
||||
};
|
||||
|
||||
const resetButtonStyle: CSSProperties = {
|
||||
...buttonStyle,
|
||||
background: 'rgba(251, 191, 36, 0.1)',
|
||||
borderColor: 'rgba(251, 191, 36, 0.2)',
|
||||
color: '#fbbf24',
|
||||
};
|
||||
|
||||
const modifiedBadgeStyle: CSSProperties = {
|
||||
fontSize: 10,
|
||||
padding: '2px 6px',
|
||||
background: 'rgba(251, 191, 36, 0.15)',
|
||||
color: '#fbbf24',
|
||||
borderRadius: 4,
|
||||
};
|
||||
|
||||
const disabledBadgeStyle: CSSProperties = {
|
||||
fontSize: 10,
|
||||
padding: '2px 6px',
|
||||
background: 'rgba(107, 114, 128, 0.2)',
|
||||
color: '#6b7280',
|
||||
borderRadius: 4,
|
||||
};
|
||||
|
||||
const formStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
};
|
||||
|
||||
const fieldStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 6,
|
||||
};
|
||||
|
||||
const labelStyle: CSSProperties = {
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#9ca3af',
|
||||
};
|
||||
|
||||
const hintStyle: CSSProperties = {
|
||||
fontSize: 11,
|
||||
color: '#6b7280',
|
||||
};
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
padding: '10px 12px',
|
||||
fontSize: 13,
|
||||
background: 'rgba(255, 255, 255, 0.03)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 6,
|
||||
color: '#e5e7eb',
|
||||
outline: 'none',
|
||||
};
|
||||
|
||||
const textareaStyle: CSSProperties = {
|
||||
...inputStyle,
|
||||
minHeight: 300,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.6,
|
||||
resize: 'vertical',
|
||||
};
|
||||
|
||||
const checkboxRowStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
gap: 24,
|
||||
};
|
||||
|
||||
const checkboxLabelStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
fontSize: 13,
|
||||
color: '#e5e7eb',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const errorStyle: CSSProperties = {
|
||||
padding: '10px 12px',
|
||||
marginBottom: 16,
|
||||
background: 'rgba(239, 68, 68, 0.1)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.2)',
|
||||
borderRadius: 6,
|
||||
color: '#ef4444',
|
||||
fontSize: 13,
|
||||
};
|
||||
Reference in New Issue
Block a user