mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-13 05:07:40 +02:00
[eric] delete unused Prompts feature, force AskUserQuestion for clarifying questions, clean up dead refs and TS errors
This commit is contained in:
@@ -1,32 +0,0 @@
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, Literal, Any
|
||||
from uuid import uuid4
|
||||
|
||||
class TemplateField(BaseModel):
|
||||
name: str
|
||||
type: Literal["str", "int", "float", "select", "multi-select", "literal"]
|
||||
options: Optional[list[str]] = None
|
||||
default: Optional[Any] = None
|
||||
required: bool = True
|
||||
|
||||
class PromptTemplate(BaseModel):
|
||||
id: str = Field(default_factory=lambda: uuid4().hex)
|
||||
name: str
|
||||
description: str = ""
|
||||
template: str # with {{field_name}} placeholders
|
||||
fields: list[TemplateField] = Field(default_factory=list)
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
class PromptTemplateCreate(BaseModel):
|
||||
name: str
|
||||
description: str = ""
|
||||
template: str
|
||||
fields: list[TemplateField] = Field(default_factory=list)
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
|
||||
class PromptTemplateUpdate(BaseModel):
|
||||
name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
template: Optional[str] = None
|
||||
fields: Optional[list[TemplateField]] = None
|
||||
tags: Optional[list[str]] = None
|
||||
@@ -1,95 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import HTTPException
|
||||
from backend.config.Apps import SubApp
|
||||
from backend.apps.templates.models import PromptTemplate, PromptTemplateCreate, PromptTemplateUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from backend.config.paths import TEMPLATES_DIR as DATA_DIR
|
||||
|
||||
@asynccontextmanager
|
||||
async def templates_lifespan():
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
yield
|
||||
|
||||
templates = SubApp("templates", templates_lifespan)
|
||||
|
||||
def _load_all() -> list[PromptTemplate]:
|
||||
result = []
|
||||
if not os.path.exists(DATA_DIR):
|
||||
return result
|
||||
for fname in os.listdir(DATA_DIR):
|
||||
if fname.endswith(".json"):
|
||||
with open(os.path.join(DATA_DIR, fname)) as f:
|
||||
result.append(PromptTemplate(**json.load(f)))
|
||||
return result
|
||||
|
||||
def _save(template: PromptTemplate):
|
||||
path = os.path.join(DATA_DIR, f"{template.id}.json")
|
||||
with open(path, "w") as f:
|
||||
json.dump(template.model_dump(), f, indent=2)
|
||||
|
||||
def _load(template_id: str) -> PromptTemplate:
|
||||
path = os.path.join(DATA_DIR, f"{template_id}.json")
|
||||
if not os.path.exists(path):
|
||||
raise HTTPException(status_code=404, detail="Template not found")
|
||||
with open(path) as f:
|
||||
return PromptTemplate(**json.load(f))
|
||||
|
||||
def _delete(template_id: str):
|
||||
path = os.path.join(DATA_DIR, f"{template_id}.json")
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
@templates.router.get("/list")
|
||||
async def list_templates():
|
||||
return {"templates": [t.model_dump() for t in _load_all()]}
|
||||
|
||||
@templates.router.get("/{template_id}")
|
||||
async def get_template(template_id: str):
|
||||
return _load(template_id).model_dump()
|
||||
|
||||
@templates.router.post("/create")
|
||||
async def create_template(body: PromptTemplateCreate):
|
||||
template = PromptTemplate(
|
||||
name=body.name,
|
||||
description=body.description,
|
||||
template=body.template,
|
||||
fields=body.fields,
|
||||
tags=body.tags,
|
||||
)
|
||||
_save(template)
|
||||
from backend.apps.analytics.collector import record as _analytics
|
||||
_analytics("feature.used", {"feature": "template.created"})
|
||||
return {"ok": True, "template": template.model_dump()}
|
||||
|
||||
@templates.router.put("/{template_id}")
|
||||
async def update_template(template_id: str, body: PromptTemplateUpdate):
|
||||
template = _load(template_id)
|
||||
update_data = body.model_dump(exclude_none=True)
|
||||
for k, v in update_data.items():
|
||||
setattr(template, k, v)
|
||||
_save(template)
|
||||
return {"ok": True, "template": template.model_dump()}
|
||||
|
||||
@templates.router.delete("/{template_id}")
|
||||
async def delete_template(template_id: str):
|
||||
_delete(template_id)
|
||||
return {"ok": True}
|
||||
|
||||
@templates.router.post("/render")
|
||||
async def render_template(body: dict):
|
||||
template_id = body.get("template_id", "")
|
||||
values = body.get("values", {})
|
||||
template = _load(template_id)
|
||||
rendered = template.template
|
||||
for field in template.fields:
|
||||
placeholder = "{{" + field.name + "}}"
|
||||
value = values.get(field.name, field.default or "")
|
||||
rendered = rendered.replace(placeholder, str(value))
|
||||
from backend.apps.analytics.collector import record as _analytics
|
||||
_analytics("feature.used", {"feature": "template.used"})
|
||||
return {"rendered": rendered}
|
||||
@@ -28,7 +28,6 @@ SESSIONS_DIR = os.path.join(DATA_ROOT, "sessions")
|
||||
TOOLS_DIR = os.path.join(DATA_ROOT, "tools")
|
||||
SETTINGS_DIR = os.path.join(DATA_ROOT, "settings")
|
||||
MODES_DIR = os.path.join(DATA_ROOT, "modes")
|
||||
TEMPLATES_DIR = os.path.join(DATA_ROOT, "templates")
|
||||
DASHBOARDS_DIR = os.path.join(DATA_ROOT, "dashboards")
|
||||
OUTPUTS_DIR = os.path.join(DATA_ROOT, "outputs")
|
||||
OUTPUTS_WORKSPACE_DIR = os.path.join(DATA_ROOT, "outputs_workspace")
|
||||
|
||||
+1
-2
@@ -13,7 +13,6 @@ from backend.config.Apps import MainApp
|
||||
from backend.apps.health.health import health
|
||||
from backend.apps.agents.agents import agents
|
||||
from backend.apps.agents.ws_manager import ws_manager
|
||||
from backend.apps.templates.templates import templates
|
||||
from backend.apps.skills.skills import skills
|
||||
from backend.apps.tools_lib.tools_lib import tools_lib
|
||||
from backend.apps.modes.modes import modes
|
||||
@@ -27,7 +26,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi import WebSocket, WebSocketDisconnect
|
||||
import json
|
||||
|
||||
main_app = MainApp([health, agents, templates, skills, tools_lib, modes, settings, mcp_registry, skill_registry, outputs, dashboards, analytics])
|
||||
main_app = MainApp([health, agents, skills, tools_lib, modes, settings, mcp_registry, skill_registry, outputs, dashboards, analytics])
|
||||
app = main_app.app
|
||||
|
||||
app.add_middleware(
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
} from '@/shared/state/updateSlice';
|
||||
import AppShell from './components/Layout/AppShell';
|
||||
import DashboardSelection from './pages/DashboardSelection/DashboardSelection';
|
||||
import Templates from './pages/Templates/Templates';
|
||||
import Skills from './pages/Skills/Skills';
|
||||
import Tools from './pages/Tools/Tools';
|
||||
import Modes from './pages/Modes/Modes';
|
||||
@@ -253,7 +252,6 @@ const ThemedApp: React.FC = () => {
|
||||
routes. This route exists only so React Router matches the URL. */}
|
||||
<Route path="/dashboard/:id" element={null} />
|
||||
<Route path="/customization" element={<Customization />} />
|
||||
<Route path="/templates" element={<Templates />} />
|
||||
<Route path="/skills" element={<Skills />} />
|
||||
<Route path="/actions" element={<Tools />} />
|
||||
<Route path="/modes" element={<Modes />} />
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { updateSettings } from '@/shared/state/settingsSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const AnalyticsOptIn: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const settings = useAppSelector((s) => s.settings.data);
|
||||
const loaded = useAppSelector((s) => s.settings.loaded);
|
||||
|
||||
if (!loaded || settings.analytics_opt_in !== null) return null;
|
||||
|
||||
const handleChoice = (optIn: boolean) => {
|
||||
dispatch(updateSettings({ ...settings, analytics_opt_in: optIn }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
bottom: 24,
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
zIndex: 1400,
|
||||
maxWidth: 480,
|
||||
width: '90%',
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2.5,
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
borderRadius: 3,
|
||||
boxShadow: c.shadow.lg,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ color: c.text.primary, fontSize: '0.9rem', fontWeight: 600, mb: 0.5 }}>
|
||||
Help improve OpenSwarm
|
||||
</Typography>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.8rem', lineHeight: 1.5, mb: 2 }}>
|
||||
Share anonymous usage statistics like session counts, feature usage, and model preferences.
|
||||
No conversations, file paths, or personal information — ever.
|
||||
You can change this anytime in Settings.
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
onClick={() => handleChoice(false)}
|
||||
sx={{
|
||||
color: c.text.muted,
|
||||
textTransform: 'none',
|
||||
fontSize: '0.82rem',
|
||||
'&:hover': { bgcolor: `${c.text.tertiary}0A` },
|
||||
}}
|
||||
>
|
||||
No thanks
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => handleChoice(true)}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
fontSize: '0.82rem',
|
||||
borderRadius: 1.5,
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
Share anonymous data
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AnalyticsOptIn;
|
||||
@@ -2,7 +2,6 @@ import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
|
||||
import QuestionAnswerOutlinedIcon from '@mui/icons-material/QuestionAnswerOutlined';
|
||||
@@ -19,7 +18,6 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { fetchSkills } from '@/shared/state/skillsSlice';
|
||||
import { fetchTemplates } from '@/shared/state/templatesSlice';
|
||||
|
||||
const XLogoIcon: React.FC<{ sx?: object }> = ({ sx }) => (
|
||||
<SvgIcon sx={sx} viewBox="0 0 24 24">
|
||||
@@ -58,7 +56,7 @@ export function getToolGroupIcon(groupName: string, size: number = 15): React.Re
|
||||
|
||||
export interface CommandPickerItem {
|
||||
id: string;
|
||||
type: 'template' | 'skill' | 'mode' | 'context';
|
||||
type: 'skill' | 'mode' | 'context';
|
||||
category: string;
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -100,7 +98,6 @@ function highlightMatch(text: string, query: string, color: string): React.React
|
||||
const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, visible }) => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const templates = useAppSelector((s) => s.templates.items);
|
||||
const skills = useAppSelector((s) => s.skills.items);
|
||||
const modesMap = useAppSelector((s) => s.modes.items);
|
||||
const builtinTools = useAppSelector((s) => s.tools.builtinTools);
|
||||
@@ -113,30 +110,18 @@ const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, vi
|
||||
const builtinLoaded = useAppSelector((s) => s.tools.builtinLoaded);
|
||||
const outputsLoaded = useAppSelector((s) => s.outputs.loaded);
|
||||
const skillsLoaded = useAppSelector((s) => s.skills.loaded);
|
||||
const templatesLoaded = useAppSelector((s) => s.templates.loaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (!builtinLoaded) dispatch(fetchBuiltinTools());
|
||||
if (!toolsLoaded) dispatch(fetchTools());
|
||||
if (!outputsLoaded) dispatch(fetchOutputs());
|
||||
if (!skillsLoaded) dispatch(fetchSkills());
|
||||
if (!templatesLoaded) dispatch(fetchTemplates());
|
||||
}, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded, skillsLoaded, templatesLoaded]);
|
||||
}, [dispatch, builtinLoaded, toolsLoaded, outputsLoaded, skillsLoaded]);
|
||||
|
||||
const items: CommandPickerItem[] = useMemo(() => {
|
||||
let all: CommandPickerItem[] = [];
|
||||
|
||||
if (trigger === '/') {
|
||||
const templateItems: CommandPickerItem[] = Object.values(templates).map((t) => ({
|
||||
id: t.id,
|
||||
type: 'template' as const,
|
||||
category: 'Templates',
|
||||
name: t.name,
|
||||
description: t.description || `Template with ${t.fields.length} fields`,
|
||||
command: t.name.toLowerCase().replace(/\s+/g, '-'),
|
||||
icon: <DescriptionIcon sx={{ fontSize: 15 }} />,
|
||||
}));
|
||||
|
||||
const skillItems: CommandPickerItem[] = Object.values(skills).map((s) => ({
|
||||
id: s.id,
|
||||
type: 'skill' as const,
|
||||
@@ -160,7 +145,7 @@ const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, vi
|
||||
};
|
||||
});
|
||||
|
||||
all = [...templateItems, ...skillItems, ...modeItems];
|
||||
all = [...skillItems, ...modeItems];
|
||||
} else {
|
||||
const atItems: CommandPickerItem[] = [
|
||||
{
|
||||
@@ -301,7 +286,7 @@ const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, vi
|
||||
item.command.toLowerCase().includes(lower) ||
|
||||
item.description.toLowerCase().includes(lower),
|
||||
);
|
||||
}, [trigger, templates, skills, modesMap, builtinTools, customTools, outputItems, filter]);
|
||||
}, [trigger, skills, modesMap, builtinTools, customTools, outputItems, filter]);
|
||||
|
||||
const flatItems = useMemo(() => {
|
||||
const result: { item: CommandPickerItem; isGroupStart: boolean; category: string }[] = [];
|
||||
@@ -315,7 +300,6 @@ const CommandPicker: React.FC<Props> = ({ trigger, filter, onSelect, onClose, vi
|
||||
|
||||
const getIconColor = (item: CommandPickerItem): string => {
|
||||
switch (item.type) {
|
||||
case 'template': return c.accent.primary;
|
||||
case 'skill': return c.status.success;
|
||||
case 'mode': {
|
||||
const mode = modesMap[item.id];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useRef, useCallback, useMemo, RefObject } from 'react';
|
||||
import React, { createContext, useContext, useState, useRef, useCallback, useMemo, MutableRefObject } from 'react';
|
||||
|
||||
export interface SelectedElement {
|
||||
id: string;
|
||||
@@ -31,7 +31,7 @@ interface ElementSelectionContextValue {
|
||||
addElementForOwner: (ownerId: string, el: SelectedElement) => void;
|
||||
removeOwnerElement: (ownerId: string, elementId: string) => void;
|
||||
clearOwnerElements: (ownerId: string) => void;
|
||||
iframeRef: RefObject<HTMLIFrameElement | null>;
|
||||
iframeRef: MutableRefObject<HTMLIFrameElement | null>;
|
||||
}
|
||||
|
||||
const ElementSelectionContext = createContext<ElementSelectionContextValue | null>(null);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const shortcuts = [
|
||||
{ key: 'd', description: 'Go to Dashboard' },
|
||||
{ key: 't', description: 'Go to Templates' },
|
||||
{ key: '1-9', description: 'Open agent by position' },
|
||||
{ key: '⌘M', description: 'Add App' },
|
||||
{ key: '⌘N', description: 'New Browser' },
|
||||
|
||||
@@ -14,7 +14,6 @@ import Snackbar from '@mui/material/Snackbar';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import InputBase from '@mui/material/InputBase';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
@@ -50,7 +49,6 @@ const SIDEBAR_WIDTH_KEY = 'openswarm-sidebar-width';
|
||||
const UPDATE_DISMISS_KEY = 'openswarm-update-dismissed';
|
||||
|
||||
const CUSTOMIZATION_ITEMS = [
|
||||
{ label: 'Prompts', path: '/templates', icon: <DescriptionIcon />, onboarding: 'sidebar-prompts' },
|
||||
{ label: 'Skills', path: '/skills', icon: <PsychologyIcon />, onboarding: 'sidebar-skills' },
|
||||
{ label: 'Actions', path: '/actions', icon: <BuildIcon />, onboarding: 'sidebar-actions' },
|
||||
{ label: 'Modes', path: '/modes', icon: <TuneIcon />, onboarding: 'sidebar-modes' },
|
||||
|
||||
@@ -333,7 +333,6 @@ const OnboardingModal: React.FC = () => {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
fontSize: '0.82rem',
|
||||
color: c.text.primary,
|
||||
bgcolor: c.bg.input,
|
||||
borderRadius: `${c.radius.md}px`,
|
||||
'& fieldset': { borderColor: c.border.subtle },
|
||||
'&:hover fieldset': { borderColor: c.border.medium },
|
||||
@@ -353,7 +352,6 @@ const OnboardingModal: React.FC = () => {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
fontSize: '0.82rem',
|
||||
color: c.text.primary,
|
||||
bgcolor: c.bg.input,
|
||||
borderRadius: `${c.radius.md}px`,
|
||||
'& fieldset': { borderColor: c.border.subtle },
|
||||
'&:hover fieldset': { borderColor: c.border.medium },
|
||||
@@ -399,7 +397,6 @@ const OnboardingModal: React.FC = () => {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
fontSize: '0.82rem',
|
||||
color: c.text.primary,
|
||||
bgcolor: c.bg.input,
|
||||
borderRadius: `${c.radius.md}px`,
|
||||
'& fieldset': { borderColor: c.border.subtle },
|
||||
'&:hover fieldset': { borderColor: c.border.medium },
|
||||
@@ -446,7 +443,6 @@ const OnboardingModal: React.FC = () => {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
fontSize: '0.82rem',
|
||||
color: c.text.primary,
|
||||
bgcolor: c.bg.input,
|
||||
borderRadius: `${c.radius.md}px`,
|
||||
'& fieldset': { borderColor: c.border.subtle },
|
||||
'&:hover fieldset': { borderColor: c.border.medium },
|
||||
@@ -465,7 +461,7 @@ const OnboardingModal: React.FC = () => {
|
||||
textTransform: 'none', fontSize: '0.82rem', fontWeight: 600,
|
||||
bgcolor: c.accent.primary, color: '#fff',
|
||||
borderRadius: `${c.radius.md}px`, py: 1,
|
||||
'&:hover': { bgcolor: c.accent.primaryHover || c.accent.primary },
|
||||
'&:hover': { bgcolor: c.accent.hover },
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -42,12 +42,6 @@ const STEPS: WalkthroughStep[] = [
|
||||
description: 'Scroll to zoom in and out. Drag the background to pan around. Click any card to focus on it.',
|
||||
placement: 'top',
|
||||
},
|
||||
{
|
||||
target: 'sidebar-prompts',
|
||||
title: 'Saved Prompts',
|
||||
description: 'Save message templates you use often \u2014 like email formats, report structures, or frequently asked questions.',
|
||||
placement: 'right',
|
||||
},
|
||||
{
|
||||
target: 'sidebar-skills',
|
||||
title: 'Skills',
|
||||
@@ -392,7 +386,7 @@ const OnboardingWalkthrough: React.FC<Props> = ({ onComplete }) => {
|
||||
py: 0.75,
|
||||
fontFamily: c.font.sans,
|
||||
visibility: currentStep === 0 || step.target === 'new-agent-button' ? 'hidden' : 'visible',
|
||||
'&:hover': { bgcolor: c.bg.hover || 'rgba(255,255,255,0.05)' },
|
||||
'&:hover': { bgcolor: 'rgba(255,255,255,0.05)' },
|
||||
}}
|
||||
>
|
||||
Back
|
||||
|
||||
@@ -13,9 +13,7 @@ import {
|
||||
TriggerState,
|
||||
EMPTY_TRIGGER,
|
||||
} from '@/app/components/richEditorUtils';
|
||||
import TemplateInvokeModal from '@/app/pages/AgentChat/TemplateInvokeModal';
|
||||
import { useAppSelector } from '@/shared/hooks';
|
||||
import { PromptTemplate } from '@/shared/state/templatesSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
interface RichPromptEditorProps {
|
||||
@@ -52,9 +50,7 @@ const RichPromptEditor: React.FC<RichPromptEditorProps> = ({
|
||||
|
||||
const [picker, setPicker] = useState<TriggerState>(EMPTY_TRIGGER);
|
||||
const [pickerRect, setPickerRect] = useState<DOMRect | null>(null);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<PromptTemplate | null>(null);
|
||||
|
||||
const templates = useAppSelector((state) => state.templates.items);
|
||||
const skills = useAppSelector((state) => state.skills.items);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -185,15 +181,7 @@ const RichPromptEditor: React.FC<RichPromptEditorProps> = ({
|
||||
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
|
||||
}
|
||||
|
||||
if (item.type === 'template') {
|
||||
const tmpl = templates[item.id];
|
||||
if (!tmpl) return;
|
||||
if (tmpl.fields.length === 0) {
|
||||
document.execCommand('insertText', false, tmpl.template);
|
||||
} else {
|
||||
setSelectedTemplate(tmpl);
|
||||
}
|
||||
} else if (item.type === 'skill') {
|
||||
if (item.type === 'skill') {
|
||||
const skill = skills[item.id];
|
||||
if (!skill) return;
|
||||
if (editor.querySelector(`[${SKILL_PILL_ATTR}="${skill.id}"]`)) return;
|
||||
@@ -363,22 +351,6 @@ const RichPromptEditor: React.FC<RichPromptEditorProps> = ({
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{selectedTemplate && (
|
||||
<TemplateInvokeModal
|
||||
template={selectedTemplate}
|
||||
open={!!selectedTemplate}
|
||||
onClose={() => setSelectedTemplate(null)}
|
||||
onApply={(rendered) => {
|
||||
const editor = editorRef.current;
|
||||
if (editor) {
|
||||
document.execCommand('insertText', false, rendered);
|
||||
}
|
||||
setSelectedTemplate(null);
|
||||
updateHasContent();
|
||||
emitChange();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import List from '@mui/material/List';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import { useAppSelector } from '@/shared/hooks';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
export interface SlashItem {
|
||||
id: string;
|
||||
type: 'template' | 'skill';
|
||||
name: string;
|
||||
description: string;
|
||||
command: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
filter: string;
|
||||
onSelect: (item: SlashItem) => void;
|
||||
onClose: () => void;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const SlashCommandPicker: React.FC<Props> = ({ filter, onSelect, onClose, visible }) => {
|
||||
const c = useClaudeTokens();
|
||||
const templates = useAppSelector((state) => state.templates.items);
|
||||
const skills = useAppSelector((state) => state.skills.items);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const items: SlashItem[] = useMemo(() => {
|
||||
const all: SlashItem[] = [
|
||||
...Object.values(templates).map((t) => ({
|
||||
id: t.id,
|
||||
type: 'template' as const,
|
||||
name: t.name,
|
||||
description: t.description || `Template with ${t.fields.length} fields`,
|
||||
command: t.name.toLowerCase().replace(/\s+/g, '-'),
|
||||
})),
|
||||
...Object.values(skills).map((s) => ({
|
||||
id: s.id,
|
||||
type: 'skill' as const,
|
||||
name: s.name,
|
||||
description: s.description || 'Skill',
|
||||
command: s.command || s.id,
|
||||
})),
|
||||
];
|
||||
|
||||
if (!filter) return all;
|
||||
const lower = filter.toLowerCase();
|
||||
return all.filter(
|
||||
(item) =>
|
||||
item.name.toLowerCase().includes(lower) ||
|
||||
item.command.toLowerCase().includes(lower) ||
|
||||
item.description.toLowerCase().includes(lower)
|
||||
);
|
||||
}, [templates, skills, filter]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
}, [filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
setSelectedIndex((prev) => Math.min(prev + 1, items.length - 1));
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
||||
} else if (e.key === 'Enter' && items[selectedIndex]) {
|
||||
e.preventDefault();
|
||||
onSelect(items[selectedIndex]);
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [visible, items, selectedIndex, onSelect, onClose]);
|
||||
|
||||
if (!visible || items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
mb: 0.5,
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
borderRadius: 3,
|
||||
maxHeight: 280,
|
||||
overflow: 'auto',
|
||||
zIndex: 1000,
|
||||
boxShadow: c.shadow.lg,
|
||||
'&::-webkit-scrollbar': { width: 5 },
|
||||
'&::-webkit-scrollbar-track': { background: 'transparent' },
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
background: c.border.medium,
|
||||
borderRadius: 3,
|
||||
'&:hover': { background: c.border.strong },
|
||||
},
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: `${c.border.medium} transparent`,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ px: 1.5, py: 1, borderBottom: `0.5px solid ${c.border.medium}` }}>
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.7rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
Commands
|
||||
</Typography>
|
||||
</Box>
|
||||
<List sx={{ py: 0.5 }}>
|
||||
{items.map((item, i) => (
|
||||
<ListItemButton
|
||||
key={`${item.type}-${item.id}`}
|
||||
selected={i === selectedIndex}
|
||||
onClick={() => onSelect(item)}
|
||||
sx={{
|
||||
py: 0.75,
|
||||
px: 1.5,
|
||||
'&.Mui-selected': { bgcolor: 'rgba(174,86,48,0.06)' },
|
||||
'&:hover': { bgcolor: 'rgba(0,0,0,0.04)' },
|
||||
}}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 32 }}>
|
||||
{item.type === 'template' ? (
|
||||
<DescriptionIcon sx={{ fontSize: 18, color: c.accent.primary }} />
|
||||
) : (
|
||||
<PsychologyIcon sx={{ fontSize: 18, color: c.status.success }} />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography sx={{ color: c.text.primary, fontSize: '0.8rem', fontWeight: 500 }}>
|
||||
/{item.command}
|
||||
</Typography>
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.7rem' }}>
|
||||
{item.type}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
secondary={
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.7rem', mt: 0.25 }}>
|
||||
{item.description}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlashCommandPicker;
|
||||
@@ -39,10 +39,8 @@ import {
|
||||
TriggerState,
|
||||
EMPTY_TRIGGER,
|
||||
} from '@/app/components/richEditorUtils';
|
||||
import TemplateInvokeModal from './TemplateInvokeModal';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import { fetchModes } from '@/shared/state/modesSlice';
|
||||
import { PromptTemplate } from '@/shared/state/templatesSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
export interface AttachedImage {
|
||||
@@ -155,8 +153,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
|
||||
attachedSkillsRef.current = attachedSkills;
|
||||
|
||||
const [picker, setPicker] = useState<TriggerState>(EMPTY_TRIGGER);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<PromptTemplate | null>(null);
|
||||
const templates = useAppSelector((state) => state.templates.items);
|
||||
const skills = useAppSelector((state) => state.skills.items);
|
||||
const modesMap = useAppSelector((state) => state.modes.items);
|
||||
const modesArr = useMemo(() => Object.values(modesMap), [modesMap]);
|
||||
@@ -427,15 +423,7 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
|
||||
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
|
||||
}
|
||||
|
||||
if (item.type === 'template') {
|
||||
const tmpl = templates[item.id];
|
||||
if (!tmpl) return;
|
||||
if (tmpl.fields.length === 0) {
|
||||
document.execCommand('insertText', false, tmpl.template);
|
||||
} else {
|
||||
setSelectedTemplate(tmpl);
|
||||
}
|
||||
} else if (item.type === 'skill') {
|
||||
if (item.type === 'skill') {
|
||||
const skill = skills[item.id];
|
||||
if (!skill) return;
|
||||
if (editor.querySelector(`[${SKILL_PILL_ATTR}="${skill.id}"]`)) return;
|
||||
@@ -1204,29 +1192,6 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ onSend, disabled, mode,
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{selectedTemplate && (
|
||||
<TemplateInvokeModal
|
||||
template={selectedTemplate}
|
||||
open={!!selectedTemplate}
|
||||
onClose={() => setSelectedTemplate(null)}
|
||||
onApply={(rendered) => {
|
||||
const editor = editorRef.current;
|
||||
if (editor) {
|
||||
editor.innerHTML = '';
|
||||
editor.textContent = rendered;
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(editor);
|
||||
range.collapse(false);
|
||||
const sel = window.getSelection();
|
||||
if (sel) { sel.removeAllRanges(); sel.addRange(range); }
|
||||
}
|
||||
setSelectedTemplate(null);
|
||||
setAttachedSkills({});
|
||||
setHasContent(!!rendered);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
open={!!lightboxSrc}
|
||||
onClose={() => setLightboxSrc(null)}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import Button from '@mui/material/Button';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import Select from '@mui/material/Select';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
import { PromptTemplate, TemplateField } from '@/shared/state/templatesSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
interface Props {
|
||||
template: PromptTemplate;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onApply: (rendered: string) => void;
|
||||
}
|
||||
|
||||
const TemplateInvokeModal: React.FC<Props> = ({ template, open, onClose, onApply }) => {
|
||||
const c = useClaudeTokens();
|
||||
const inputSx = {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
color: c.text.primary,
|
||||
'& fieldset': { borderColor: c.border.strong },
|
||||
'&:hover fieldset': { borderColor: c.text.tertiary },
|
||||
'&.Mui-focused fieldset': { borderColor: c.accent.primary },
|
||||
},
|
||||
'& .MuiInputLabel-root': { color: c.text.tertiary },
|
||||
'& .MuiInputLabel-root.Mui-focused': { color: c.accent.primary },
|
||||
};
|
||||
const [values, setValues] = useState<Record<string, any>>(() => {
|
||||
const init: Record<string, any> = {};
|
||||
for (const f of template.fields) {
|
||||
init[f.name] = f.default ?? (f.type === 'multi-select' ? [] : '');
|
||||
}
|
||||
return init;
|
||||
});
|
||||
|
||||
const handleApply = () => {
|
||||
let rendered = template.template;
|
||||
for (const f of template.fields) {
|
||||
const val = values[f.name];
|
||||
const str = Array.isArray(val) ? val.join(', ') : String(val ?? '');
|
||||
rendered = rendered.replace(new RegExp(`\\{\\{${f.name}\\}\\}`, 'g'), str);
|
||||
}
|
||||
onApply(rendered);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const renderField = (field: TemplateField) => {
|
||||
const val = values[field.name];
|
||||
const update = (v: any) => setValues((prev) => ({ ...prev, [field.name]: v }));
|
||||
|
||||
switch (field.type) {
|
||||
case 'literal':
|
||||
return (
|
||||
<Box key={field.name} sx={{ mb: 2 }}>
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.75rem', mb: 0.5 }}>{field.name}</Typography>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem', fontFamily: c.font.mono, bgcolor: c.bg.secondary, p: 1, borderRadius: 1.5 }}>
|
||||
{field.default || ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
case 'select':
|
||||
return (
|
||||
<TextField
|
||||
key={field.name}
|
||||
select
|
||||
label={field.name}
|
||||
value={val || ''}
|
||||
onChange={(e) => update(e.target.value)}
|
||||
fullWidth
|
||||
size="small"
|
||||
sx={{ ...inputSx, mb: 2 }}
|
||||
SelectProps={{ MenuProps: { PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } } }}
|
||||
>
|
||||
{(field.options || []).map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>{opt}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
);
|
||||
case 'multi-select':
|
||||
return (
|
||||
<FormControl key={field.name} fullWidth size="small" sx={{ mb: 2, ...inputSx }}>
|
||||
<InputLabel sx={{ color: c.text.tertiary }}>{field.name}</InputLabel>
|
||||
<Select
|
||||
multiple
|
||||
value={Array.isArray(val) ? val : []}
|
||||
onChange={(e) => update(e.target.value)}
|
||||
input={<OutlinedInput label={field.name} />}
|
||||
renderValue={(selected: string[]) => selected.join(', ')}
|
||||
MenuProps={{ PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } }}
|
||||
>
|
||||
{(field.options || []).map((opt) => (
|
||||
<MenuItem key={opt} value={opt}>
|
||||
<Checkbox checked={(val || []).includes(opt)} sx={{ color: c.text.tertiary }} />
|
||||
<ListItemText primary={opt} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
case 'int':
|
||||
case 'float':
|
||||
return (
|
||||
<TextField
|
||||
key={field.name}
|
||||
label={field.name}
|
||||
type="number"
|
||||
value={val ?? ''}
|
||||
onChange={(e) => update(field.type === 'int' ? parseInt(e.target.value) || '' : parseFloat(e.target.value) || '')}
|
||||
fullWidth
|
||||
size="small"
|
||||
sx={{ ...inputSx, mb: 2 }}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<TextField
|
||||
key={field.name}
|
||||
label={field.name}
|
||||
value={val || ''}
|
||||
onChange={(e) => update(e.target.value)}
|
||||
fullWidth
|
||||
size="small"
|
||||
multiline={field.name.toLowerCase().includes('description') || field.name.toLowerCase().includes('prompt')}
|
||||
rows={field.name.toLowerCase().includes('description') || field.name.toLowerCase().includes('prompt') ? 3 : 1}
|
||||
sx={{ ...inputSx, mb: 2 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
bgcolor: c.bg.surface,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle sx={{ color: c.text.primary, fontWeight: 600 }}>{template.name}</DialogTitle>
|
||||
<DialogContent>
|
||||
{template.description && (
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.85rem', mb: 2 }}>{template.description}</Typography>
|
||||
)}
|
||||
{template.fields.length === 0 ? (
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem' }}>
|
||||
This template has no input fields. It will be inserted as-is.
|
||||
</Typography>
|
||||
) : (
|
||||
template.fields.map(renderField)
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={onClose} sx={{ color: c.text.tertiary }}>Cancel</Button>
|
||||
<Button
|
||||
onClick={handleApply}
|
||||
variant="contained"
|
||||
sx={{ bgcolor: c.accent.primary, '&:hover': { bgcolor: c.accent.hover }, fontWeight: 600 }}
|
||||
>
|
||||
Apply Template
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateInvokeModal;
|
||||
@@ -2,7 +2,6 @@ import React, { useEffect, useMemo } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
|
||||
import AlternateEmailIcon from '@mui/icons-material/AlternateEmail';
|
||||
@@ -17,13 +16,12 @@ import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
|
||||
import { getToolGroupIcon } from '@/app/components/CommandPicker';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { fetchTemplates } from '@/shared/state/templatesSlice';
|
||||
import { fetchSkills } from '@/shared/state/skillsSlice';
|
||||
import { fetchModes } from '@/shared/state/modesSlice';
|
||||
|
||||
interface SlashCommand {
|
||||
id: string;
|
||||
type: 'template' | 'skill' | 'mode';
|
||||
type: 'skill' | 'mode';
|
||||
name: string;
|
||||
description: string;
|
||||
command: string;
|
||||
@@ -46,7 +44,6 @@ interface Shortcut {
|
||||
|
||||
const SHORTCUTS: Shortcut[] = [
|
||||
{ key: 'd', description: 'Go to Dashboard', category: 'navigation' },
|
||||
{ key: 't', description: 'Go to Templates', category: 'navigation' },
|
||||
{ key: '1-9', description: 'Open agent by position', category: 'navigation' },
|
||||
{ key: 'Shift+A', description: 'Approve all pending', category: 'action' },
|
||||
{ key: 'Shift+D', description: 'Deny all pending', category: 'action' },
|
||||
@@ -115,14 +112,12 @@ const SectionHeader: React.FC<{
|
||||
export const CommandsContent: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
const dispatch = useAppDispatch();
|
||||
const templates = useAppSelector((state) => state.templates.items);
|
||||
const skills = useAppSelector((state) => state.skills.items);
|
||||
const modesMap = useAppSelector((state) => state.modes.items);
|
||||
const builtinTools = useAppSelector((state) => state.tools.builtinTools);
|
||||
const customTools = useAppSelector((state) => state.tools.items);
|
||||
const outputItems = useAppSelector((state) => state.outputs.items);
|
||||
|
||||
const templatesLoaded = useAppSelector((state) => state.templates.loaded);
|
||||
const skillsLoaded = useAppSelector((state) => state.skills.loaded);
|
||||
const modesLoaded = useAppSelector((state) => state.modes.loaded);
|
||||
const builtinLoaded = useAppSelector((state) => state.tools.builtinLoaded);
|
||||
@@ -130,22 +125,14 @@ export const CommandsContent: React.FC = () => {
|
||||
const outputsLoaded = useAppSelector((state) => state.outputs.loaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (!templatesLoaded) dispatch(fetchTemplates());
|
||||
if (!skillsLoaded) dispatch(fetchSkills());
|
||||
if (!modesLoaded) dispatch(fetchModes());
|
||||
if (!builtinLoaded) dispatch(fetchBuiltinTools());
|
||||
if (!toolsLoaded) dispatch(fetchTools());
|
||||
if (!outputsLoaded) dispatch(fetchOutputs());
|
||||
}, [dispatch, templatesLoaded, skillsLoaded, modesLoaded, builtinLoaded, toolsLoaded, outputsLoaded]);
|
||||
}, [dispatch, skillsLoaded, modesLoaded, builtinLoaded, toolsLoaded, outputsLoaded]);
|
||||
|
||||
const slashCommands: SlashCommand[] = useMemo(() => [
|
||||
...Object.values(templates).map((t) => ({
|
||||
id: t.id,
|
||||
type: 'template' as const,
|
||||
name: t.name,
|
||||
description: t.description || `Template with ${t.fields.length} fields`,
|
||||
command: t.name.toLowerCase().replace(/\s+/g, '-'),
|
||||
})),
|
||||
...Object.values(skills).map((s) => ({
|
||||
id: s.id,
|
||||
type: 'skill' as const,
|
||||
@@ -160,7 +147,7 @@ export const CommandsContent: React.FC = () => {
|
||||
description: m.description || 'Switch to this mode',
|
||||
command: m.name.toLowerCase().replace(/\s+/g, '-'),
|
||||
})),
|
||||
], [templates, skills, modesMap]);
|
||||
], [skills, modesMap]);
|
||||
|
||||
const atCommands: AtCommand[] = useMemo(() => {
|
||||
const items: AtCommand[] = [
|
||||
@@ -272,7 +259,7 @@ export const CommandsContent: React.FC = () => {
|
||||
<SectionHeader
|
||||
icon={<TerminalIcon sx={{ fontSize: 22 }} />}
|
||||
title="Slash Commands"
|
||||
subtitle="Type / in chat to invoke templates, skills, and modes"
|
||||
subtitle="Type / in chat to invoke skills and modes"
|
||||
count={slashCommands.length}
|
||||
c={c}
|
||||
/>
|
||||
@@ -290,7 +277,7 @@ export const CommandsContent: React.FC = () => {
|
||||
>
|
||||
<TerminalIcon sx={{ fontSize: 36, opacity: 0.3 }} />
|
||||
<Typography sx={{ fontSize: '0.85rem' }}>
|
||||
No slash commands yet. Create templates, skills, or modes to see them here.
|
||||
No slash commands yet. Create skills or modes to see them here.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -310,14 +297,11 @@ export const CommandsContent: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
color: cmd.type === 'template' ? c.accent.primary
|
||||
: cmd.type === 'mode' ? (modesMap[cmd.id]?.color || c.accent.primary)
|
||||
color: cmd.type === 'mode' ? (modesMap[cmd.id]?.color || c.accent.primary)
|
||||
: c.status.success,
|
||||
display: 'flex',
|
||||
}}>
|
||||
{cmd.type === 'template' ? (
|
||||
<DescriptionIcon sx={{ fontSize: 18 }} />
|
||||
) : cmd.type === 'mode' ? (
|
||||
{cmd.type === 'mode' ? (
|
||||
<SmartToyOutlinedIcon sx={{ fontSize: 18 }} />
|
||||
) : (
|
||||
<PsychologyIcon sx={{ fontSize: 18 }} />
|
||||
@@ -342,11 +326,9 @@ export const CommandsContent: React.FC = () => {
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
bgcolor: cmd.type === 'template' ? `${c.accent.primary}12`
|
||||
: cmd.type === 'mode' ? `${modesMap[cmd.id]?.color || c.accent.primary}15`
|
||||
bgcolor: cmd.type === 'mode' ? `${modesMap[cmd.id]?.color || c.accent.primary}15`
|
||||
: `${c.status.success}15`,
|
||||
color: cmd.type === 'template' ? c.accent.primary
|
||||
: cmd.type === 'mode' ? (modesMap[cmd.id]?.color || c.accent.primary)
|
||||
color: cmd.type === 'mode' ? (modesMap[cmd.id]?.color || c.accent.primary)
|
||||
: c.status.success,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -4,20 +4,12 @@ import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardActionArea from '@mui/material/CardActionArea';
|
||||
import DescriptionIcon from '@mui/icons-material/Description';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import TuneIcon from '@mui/icons-material/Tune';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const PANELS = [
|
||||
{
|
||||
label: 'Prompts',
|
||||
path: '/templates',
|
||||
icon: <DescriptionIcon />,
|
||||
description:
|
||||
'Create and manage reusable prompt templates with structured input fields that your agents can fill in.',
|
||||
},
|
||||
{
|
||||
label: 'Skills',
|
||||
path: '/skills',
|
||||
|
||||
@@ -138,16 +138,13 @@ const BrowserCard: React.FC<Props> = ({
|
||||
|
||||
const [tabLocalStates, setTabLocalStates] = useState<Record<string, TabLocalState>>({});
|
||||
const updateTabLocal = useCallback((tabId: string, update: Partial<TabLocalState>) => {
|
||||
setTabLocalStates((prev) => ({
|
||||
...prev,
|
||||
[tabId]: {
|
||||
loading: false,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
...prev[tabId],
|
||||
...update,
|
||||
},
|
||||
}));
|
||||
setTabLocalStates((prev) => {
|
||||
const existing = prev[tabId] ?? { loading: false, canGoBack: false, canGoForward: false };
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: { ...existing, ...update },
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId);
|
||||
@@ -744,7 +741,7 @@ const BrowserCard: React.FC<Props> = ({
|
||||
transform: isBeingDragged ? `translateX(${dragTabOffset}px)` : 'none',
|
||||
transition: isBeingDragged ? 'none' : 'background 0.15s ease, transform 0.2s ease',
|
||||
zIndex: isBeingDragged ? 10 : 1,
|
||||
'&:hover': { bgcolor: isActive ? c.bg.surface : c.bg.hover },
|
||||
'&:hover': { bgcolor: isActive ? c.bg.surface : c.bg.secondary },
|
||||
'&:hover .tab-close': { opacity: 1 },
|
||||
...(isActive && {
|
||||
'&::after': {
|
||||
|
||||
@@ -262,7 +262,7 @@ const CategoryGroup: React.FC<{
|
||||
c: ReturnType<typeof useClaudeTokens>;
|
||||
children: React.ReactNode;
|
||||
}> = ({ icon, label, count, c, children }) => (
|
||||
<Box sx={{ '&:not(:first-of-type)': { borderTop: `1px solid ${c.border.light}`, mt: 0.5, pt: 0.5 } }}>
|
||||
<Box sx={{ '&:not(:first-of-type)': { borderTop: `1px solid ${c.border.subtle}`, mt: 0.5, pt: 0.5 } }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
Mode,
|
||||
} from '@/shared/state/modesSlice';
|
||||
import { fetchBuiltinTools, fetchTools } from '@/shared/state/toolsSlice';
|
||||
import { fetchTemplates } from '@/shared/state/templatesSlice';
|
||||
import { fetchSkills } from '@/shared/state/skillsSlice';
|
||||
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
|
||||
import ExtensionIcon from '@mui/icons-material/Extension';
|
||||
@@ -126,7 +125,6 @@ const Modes: React.FC = () => {
|
||||
dispatch(fetchModes());
|
||||
dispatch(fetchBuiltinTools());
|
||||
dispatch(fetchTools());
|
||||
dispatch(fetchTemplates());
|
||||
dispatch(fetchSkills());
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
@@ -1322,7 +1322,6 @@ const Settings: React.FC = () => {
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={handleCheckForUpdates}
|
||||
disabled={updateStatus === 'checking'}
|
||||
startIcon={<SystemUpdateAltIcon sx={{ fontSize: 15 }} />}
|
||||
sx={{
|
||||
color: c.text.secondary,
|
||||
|
||||
@@ -1,474 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
|
||||
import {
|
||||
fetchTemplates,
|
||||
createTemplate,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
PromptTemplate,
|
||||
TemplateField,
|
||||
} from '@/shared/state/templatesSlice';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
const FIELD_TYPES = ['str', 'int', 'float', 'select', 'multi-select', 'literal'] as const;
|
||||
|
||||
interface EditorState {
|
||||
name: string;
|
||||
description: string;
|
||||
template: string;
|
||||
fields: TemplateField[];
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const emptyEditor: EditorState = {
|
||||
name: '',
|
||||
description: '',
|
||||
template: '',
|
||||
fields: [],
|
||||
tags: [],
|
||||
};
|
||||
|
||||
const Templates: React.FC = () => {
|
||||
const c = useClaudeTokens();
|
||||
|
||||
const inputSx = {
|
||||
'& .MuiOutlinedInput-root': {
|
||||
color: c.text.primary,
|
||||
'& fieldset': { borderColor: c.border.strong },
|
||||
'&:hover fieldset': { borderColor: c.text.tertiary },
|
||||
'&.Mui-focused fieldset': { borderColor: c.accent.primary },
|
||||
},
|
||||
'& .MuiInputLabel-root': { color: c.text.tertiary },
|
||||
'& .MuiInputLabel-root.Mui-focused': { color: c.accent.primary },
|
||||
};
|
||||
|
||||
const claudePaperProps = {
|
||||
sx: {
|
||||
bgcolor: c.bg.surface,
|
||||
color: c.text.primary,
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
maxHeight: '90vh',
|
||||
},
|
||||
};
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const { items, loading } = useAppSelector((s) => s.templates);
|
||||
const templates = Object.values(items);
|
||||
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editor, setEditor] = useState<EditorState>(emptyEditor);
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchTemplates());
|
||||
}, [dispatch]);
|
||||
|
||||
const openNew = () => {
|
||||
setEditingId(null);
|
||||
setEditor(emptyEditor);
|
||||
setTagInput('');
|
||||
setEditorOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (t: PromptTemplate) => {
|
||||
setEditingId(t.id);
|
||||
setEditor({
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
template: t.template,
|
||||
fields: t.fields.map((f) => ({ ...f })),
|
||||
tags: [...t.tags],
|
||||
});
|
||||
setTagInput('');
|
||||
setEditorOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editor.name.trim() || !editor.template.trim()) return;
|
||||
if (editingId) {
|
||||
await dispatch(updateTemplate({ id: editingId, ...editor }));
|
||||
} else {
|
||||
await dispatch(createTemplate(editor));
|
||||
}
|
||||
setEditorOpen(false);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await dispatch(deleteTemplate(id));
|
||||
};
|
||||
|
||||
const addField = () => {
|
||||
setEditor((prev) => ({
|
||||
...prev,
|
||||
fields: [...prev.fields, { name: '', type: 'str', required: true }],
|
||||
}));
|
||||
};
|
||||
|
||||
const updateField = (idx: number, patch: Partial<TemplateField>) => {
|
||||
setEditor((prev) => ({
|
||||
...prev,
|
||||
fields: prev.fields.map((f, i) => (i === idx ? { ...f, ...patch } : f)),
|
||||
}));
|
||||
};
|
||||
|
||||
const removeField = (idx: number) => {
|
||||
setEditor((prev) => ({
|
||||
...prev,
|
||||
fields: prev.fields.filter((_, i) => i !== idx),
|
||||
}));
|
||||
};
|
||||
|
||||
const addTag = () => {
|
||||
const tag = tagInput.trim();
|
||||
if (tag && !editor.tags.includes(tag)) {
|
||||
setEditor((prev) => ({ ...prev, tags: [...prev.tags, tag] }));
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tag: string) => {
|
||||
setEditor((prev) => ({ ...prev, tags: prev.tags.filter((t) => t !== tag) }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ p: 3, height: '100%', overflow: 'auto' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ color: c.text.primary, fontWeight: 700, mb: 0.5 }}>
|
||||
Prompt Templates
|
||||
</Typography>
|
||||
<Typography sx={{ color: c.text.tertiary, fontSize: '0.85rem' }}>
|
||||
Create and manage reusable prompt templates with structured input fields.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="contained"
|
||||
onClick={openNew}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
borderRadius: 2,
|
||||
}}
|
||||
>
|
||||
New Template
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}>
|
||||
<CircularProgress sx={{ color: c.accent.primary }} />
|
||||
</Box>
|
||||
) : templates.length === 0 ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '50vh',
|
||||
color: c.text.ghost,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '1.1rem', mb: 1 }}>No templates yet</Typography>
|
||||
<Typography sx={{ fontSize: '0.85rem' }}>Click "New Template" to get started.</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{templates.map((t) => (
|
||||
<Box
|
||||
key={t.id}
|
||||
sx={{
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
borderRadius: 3,
|
||||
p: 2.5,
|
||||
cursor: 'pointer',
|
||||
boxShadow: c.shadow.sm,
|
||||
transition: 'border-color 0.2s, box-shadow 0.2s',
|
||||
'&:hover': {
|
||||
borderColor: c.accent.primary,
|
||||
boxShadow: '0 0 0 1px rgba(174,86,48,0.15)',
|
||||
},
|
||||
}}
|
||||
onClick={() => openEdit(t)}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography sx={{ color: c.text.primary, fontWeight: 600, fontSize: '1rem' }}>
|
||||
{t.name}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 0.5, ml: 1, flexShrink: 0 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); openEdit(t); }}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.accent.primary } }}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); handleDelete(t.id); }}
|
||||
sx={{ color: c.text.tertiary, '&:hover': { color: c.status.error } }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
{t.description && (
|
||||
<Typography
|
||||
sx={{
|
||||
color: c.text.tertiary,
|
||||
fontSize: '0.8rem',
|
||||
mb: 1.5,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{t.description}
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Chip
|
||||
label={`${t.fields.length} field${t.fields.length !== 1 ? 's' : ''}`}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'rgba(174,86,48,0.08)',
|
||||
color: c.accent.primary,
|
||||
fontSize: '0.7rem',
|
||||
height: 22,
|
||||
}}
|
||||
/>
|
||||
{t.tags.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: c.bg.secondary,
|
||||
color: c.text.muted,
|
||||
fontSize: '0.7rem',
|
||||
height: 22,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Editor Dialog */}
|
||||
<Dialog
|
||||
open={editorOpen}
|
||||
onClose={() => setEditorOpen(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={claudePaperProps}
|
||||
>
|
||||
<DialogTitle sx={{ color: c.text.primary, fontWeight: 600 }}>
|
||||
{editingId ? 'Edit Template' : 'New Template'}
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: '8px !important' }}>
|
||||
<TextField
|
||||
label="Name"
|
||||
value={editor.name}
|
||||
onChange={(e) => setEditor((p) => ({ ...p, name: e.target.value }))}
|
||||
fullWidth
|
||||
size="small"
|
||||
sx={inputSx}
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
value={editor.description}
|
||||
onChange={(e) => setEditor((p) => ({ ...p, description: e.target.value }))}
|
||||
fullWidth
|
||||
size="small"
|
||||
multiline
|
||||
rows={2}
|
||||
sx={inputSx}
|
||||
/>
|
||||
<TextField
|
||||
label="Template (use {{field_name}} for placeholders)"
|
||||
value={editor.template}
|
||||
onChange={(e) => setEditor((p) => ({ ...p, template: e.target.value }))}
|
||||
fullWidth
|
||||
size="small"
|
||||
multiline
|
||||
rows={5}
|
||||
sx={{
|
||||
...inputSx,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
...inputSx['& .MuiOutlinedInput-root'],
|
||||
fontFamily: c.font.mono,
|
||||
fontSize: '0.85rem',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Fields */}
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem', fontWeight: 600 }}>
|
||||
Fields
|
||||
</Typography>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={addField}
|
||||
sx={{ color: c.accent.primary, textTransform: 'none', fontSize: '0.8rem' }}
|
||||
>
|
||||
Add Field
|
||||
</Button>
|
||||
</Box>
|
||||
{editor.fields.map((field, idx) => (
|
||||
<Box
|
||||
key={idx}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
mb: 1,
|
||||
alignItems: 'flex-start',
|
||||
bgcolor: c.bg.elevated,
|
||||
p: 1.5,
|
||||
borderRadius: 2,
|
||||
border: `1px solid ${c.border.subtle}`,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
label="Field name"
|
||||
value={field.name}
|
||||
onChange={(e) => updateField(idx, { name: e.target.value })}
|
||||
size="small"
|
||||
sx={{ ...inputSx, flex: 1 }}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
label="Type"
|
||||
value={field.type}
|
||||
onChange={(e) => updateField(idx, { type: e.target.value as TemplateField['type'] })}
|
||||
size="small"
|
||||
sx={{ ...inputSx, minWidth: 130 }}
|
||||
SelectProps={{ MenuProps: { PaperProps: { sx: { bgcolor: c.bg.surface, color: c.text.primary } } } }}
|
||||
>
|
||||
{FIELD_TYPES.map((ft) => (
|
||||
<MenuItem key={ft} value={ft}>{ft}</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{(field.type === 'select' || field.type === 'multi-select') && (
|
||||
<TextField
|
||||
label="Options (comma-sep)"
|
||||
value={(field.options || []).join(', ')}
|
||||
onChange={(e) =>
|
||||
updateField(idx, {
|
||||
options: e.target.value.split(',').map((s) => s.trim()).filter(Boolean),
|
||||
})
|
||||
}
|
||||
size="small"
|
||||
sx={{ ...inputSx, flex: 1 }}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
label="Default"
|
||||
value={field.default ?? ''}
|
||||
onChange={(e) => updateField(idx, { default: e.target.value || undefined })}
|
||||
size="small"
|
||||
sx={{ ...inputSx, flex: 0.7 }}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => removeField(idx)}
|
||||
sx={{ color: c.text.tertiary, mt: 0.5, '&:hover': { color: c.status.error } }}
|
||||
>
|
||||
<RemoveCircleOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Tags */}
|
||||
<Box>
|
||||
<Typography sx={{ color: c.text.muted, fontSize: '0.85rem', fontWeight: 600, mb: 1 }}>
|
||||
Tags
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{editor.tags.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
label={tag}
|
||||
size="small"
|
||||
onDelete={() => removeTag(tag)}
|
||||
sx={{
|
||||
bgcolor: c.bg.secondary,
|
||||
color: c.text.muted,
|
||||
'& .MuiChip-deleteIcon': { color: c.text.tertiary, '&:hover': { color: c.status.error } },
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<TextField
|
||||
placeholder="Add tag..."
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addTag();
|
||||
}
|
||||
}}
|
||||
size="small"
|
||||
sx={{ ...inputSx, width: 140 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 2 }}>
|
||||
<Button onClick={() => setEditorOpen(false)} sx={{ color: c.text.tertiary }}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="contained"
|
||||
disabled={!editor.name.trim() || !editor.template.trim()}
|
||||
sx={{
|
||||
bgcolor: c.accent.primary,
|
||||
'&:hover': { bgcolor: c.accent.pressed },
|
||||
'&.Mui-disabled': { bgcolor: c.bg.secondary, color: c.text.ghost },
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
{editingId ? 'Save Changes' : 'Create Template'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Templates;
|
||||
@@ -110,7 +110,7 @@ interface Integration {
|
||||
credentialFields?: CredentialField[];
|
||||
connectLabel?: string;
|
||||
connectInstructions?: string;
|
||||
authType?: 'none' | 'oauth2' | 'env_vars';
|
||||
authType?: 'none' | 'oauth2' | 'env_vars' | 'device_code';
|
||||
}
|
||||
|
||||
const INTEGRATIONS: Integration[] = [
|
||||
@@ -186,7 +186,7 @@ const INTEGRATIONS: Integration[] = [
|
||||
<path d="M11.4 24H0V12.6L11.4 24zM24 24H12.6V12.6L24 24zM11.4 11.4H0V0l11.4 11.4zM24 11.4H12.6V0L24 11.4z" fill="#0078D4"/>
|
||||
</svg>
|
||||
),
|
||||
authType: 'device_code' as any,
|
||||
authType: 'device_code',
|
||||
},
|
||||
{
|
||||
id: 'notion',
|
||||
@@ -1179,14 +1179,13 @@ const Tools: React.FC = () => {
|
||||
<Box sx={{
|
||||
width: 28, height: 28, borderRadius: 1.5, flexShrink: 0,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
bgcolor: `${out.color}20`,
|
||||
bgcolor: `${c.accent.primary}20`,
|
||||
}}>
|
||||
<ViewQuiltIcon sx={{ fontSize: 14, color: out.color }} />
|
||||
<ViewQuiltIcon sx={{ fontSize: 14, color: c.accent.primary }} />
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography sx={{ color: c.text.primary, fontSize: '0.8rem', fontWeight: 500 }}>{out.name}</Typography>
|
||||
<Chip label={out.category} size="small" sx={{ bgcolor: `${out.color}15`, color: out.color, fontSize: '0.65rem', height: 18, '& .MuiChip-label': { px: 0.6 } }} />
|
||||
</Box>
|
||||
{out.description && (
|
||||
<Typography sx={{ color: c.text.ghost, fontSize: '0.7rem', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||
|
||||
@@ -58,17 +58,12 @@ function getFileIcon(filename: string): React.ReactNode {
|
||||
}
|
||||
}
|
||||
|
||||
function getEditorLanguage(filename: string): string {
|
||||
function getEditorLanguage(filename: string): 'html' | 'python' | 'json' {
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
switch (ext) {
|
||||
case 'html': case 'htm': return 'html';
|
||||
case 'py': return 'python';
|
||||
case 'json': return 'json';
|
||||
case 'js': case 'jsx': return 'javascript';
|
||||
case 'ts': case 'tsx': return 'typescript';
|
||||
case 'css': case 'scss': return 'css';
|
||||
case 'md': return 'markdown';
|
||||
default: return 'plaintext';
|
||||
default: return 'html';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +211,7 @@ const LogEntry: React.FC<LogEntryProps> = ({ msg, c }) => {
|
||||
interface AutoRunLogProps {
|
||||
messages: AgentMessage[];
|
||||
status: string | null;
|
||||
logEndRef: React.RefObject<HTMLDivElement | null>;
|
||||
logEndRef: React.RefObject<HTMLDivElement>;
|
||||
c: ReturnType<typeof useClaudeTokens>;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ export interface BrowserWebview extends HTMLElement {
|
||||
executeJavaScript: (code: string) => Promise<any>;
|
||||
sendInputEvent: (event: any) => void;
|
||||
getWebContentsId: () => number;
|
||||
addEventListener: (event: string, listener: (...args: any[]) => void) => void;
|
||||
removeEventListener: (event: string, listener: (...args: any[]) => void) => void;
|
||||
addEventListener: (event: string, listener: (...args: any[]) => void, options?: boolean | AddEventListenerOptions) => void;
|
||||
removeEventListener: (event: string, listener: (...args: any[]) => void, options?: boolean | EventListenerOptions) => void;
|
||||
}
|
||||
|
||||
const registry = new Map<string, BrowserWebview>();
|
||||
|
||||
@@ -22,10 +22,6 @@ export function useKeyboardShortcuts() {
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
if (e.key === 't' && !e.metaKey && !e.ctrlKey) {
|
||||
navigate('/templates');
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'A' && e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
||||
for (const session of Object.values(sessions)) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import tempStateReducer from './tempStateSlice';
|
||||
import agentsReducer from './agentsSlice';
|
||||
import templatesReducer from './templatesSlice';
|
||||
import skillsReducer from './skillsSlice';
|
||||
import toolsReducer from './toolsSlice';
|
||||
import modesReducer from './modesSlice';
|
||||
@@ -19,7 +18,6 @@ export const store = configureStore({
|
||||
reducer: {
|
||||
tempState: tempStateReducer,
|
||||
agents: agentsReducer,
|
||||
templates: templatesReducer,
|
||||
skills: skillsReducer,
|
||||
tools: toolsReducer,
|
||||
modes: modesReducer,
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
|
||||
const TEMPLATES_API = `${API_BASE}/templates`;
|
||||
|
||||
export interface TemplateField {
|
||||
name: string;
|
||||
type: 'str' | 'int' | 'float' | 'select' | 'multi-select' | 'literal';
|
||||
options?: string[];
|
||||
default?: any;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export interface PromptTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
template: string;
|
||||
fields: TemplateField[];
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface TemplatesState {
|
||||
items: Record<string, PromptTemplate>;
|
||||
loading: boolean;
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
const initialState: TemplatesState = {
|
||||
items: {},
|
||||
loading: false,
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
export const fetchTemplates = createAsyncThunk(
|
||||
'templates/fetch',
|
||||
async () => {
|
||||
const res = await fetch(`${TEMPLATES_API}/list`);
|
||||
const data = await res.json();
|
||||
return data.templates as PromptTemplate[];
|
||||
},
|
||||
{ condition: (_, { getState }) => !(getState() as { templates: TemplatesState }).templates.loading },
|
||||
);
|
||||
|
||||
export const createTemplate = createAsyncThunk(
|
||||
'templates/create',
|
||||
async (body: Omit<PromptTemplate, 'id'>) => {
|
||||
const res = await fetch(`${TEMPLATES_API}/create`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.template as PromptTemplate;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateTemplate = createAsyncThunk(
|
||||
'templates/update',
|
||||
async ({ id, ...updates }: Partial<PromptTemplate> & { id: string }) => {
|
||||
const res = await fetch(`${TEMPLATES_API}/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.template as PromptTemplate;
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteTemplate = createAsyncThunk('templates/delete', async (id: string) => {
|
||||
await fetch(`${TEMPLATES_API}/${id}`, { method: 'DELETE' });
|
||||
return id;
|
||||
});
|
||||
|
||||
export const renderTemplate = createAsyncThunk(
|
||||
'templates/render',
|
||||
async ({ templateId, values }: { templateId: string; values: Record<string, any> }) => {
|
||||
const res = await fetch(`${TEMPLATES_API}/render`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ template_id: templateId, values }),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.rendered as string;
|
||||
}
|
||||
);
|
||||
|
||||
const templatesSlice = createSlice({
|
||||
name: 'templates',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchTemplates.pending, (state) => { state.loading = true; })
|
||||
.addCase(fetchTemplates.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.loaded = true;
|
||||
state.items = {};
|
||||
for (const t of action.payload) {
|
||||
state.items[t.id] = t;
|
||||
}
|
||||
})
|
||||
.addCase(fetchTemplates.rejected, (state) => { state.loading = false; state.loaded = true; })
|
||||
.addCase(createTemplate.fulfilled, (state, action) => {
|
||||
state.items[action.payload.id] = action.payload;
|
||||
})
|
||||
.addCase(updateTemplate.fulfilled, (state, action) => {
|
||||
state.items[action.payload.id] = action.payload;
|
||||
})
|
||||
.addCase(deleteTemplate.fulfilled, (state, action) => {
|
||||
delete state.items[action.payload];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default templatesSlice.reducer;
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user