feat: Add AgentBar arc dock with per-agent terminal frame and voice modal
- Add ui.json configs for Main (purple) and Ejecutor (red) agents - AgentBar: fused arc-shaped dock at bottom with dynamic glow - Quick press opens styled terminal frame mockup - Hold opens voice modal with Web Speech API streaming transcription - Responsive: full-width mobile, max-width on tablet/desktop/4K - Agents API: serve uiConfig from ui.json in agent directories - Agents page: route, store, toolbar integration
This commit is contained in:
204
frontend/src/stores/agents.ts
Normal file
204
frontend/src/stores/agents.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
type FileCategory = 'config' | 'instructions' | 'plugins' | 'history' | 'debug' | 'cache' | 'sessions' | 'backups' | 'other'
|
||||
|
||||
interface AgentFile {
|
||||
name: string
|
||||
path: string
|
||||
type: 'json' | 'markdown' | 'text' | 'jsonl'
|
||||
category: FileCategory
|
||||
size: number
|
||||
}
|
||||
|
||||
interface Agent {
|
||||
id: string
|
||||
name: string
|
||||
directory: string
|
||||
files: AgentFile[]
|
||||
}
|
||||
|
||||
interface OpenFile {
|
||||
path: string
|
||||
name: string
|
||||
type: string
|
||||
content: string
|
||||
originalContent: string
|
||||
agentId: string
|
||||
category: FileCategory
|
||||
}
|
||||
|
||||
export interface CategoryInfo {
|
||||
key: FileCategory
|
||||
label: string
|
||||
icon: string // SVG path
|
||||
color: string
|
||||
files: AgentFile[]
|
||||
}
|
||||
|
||||
const CATEGORY_META: Record<FileCategory, { label: string; icon: string; color: string; order: number }> = {
|
||||
config: { label: 'Configuration', icon: 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z|M12 12m-3 0a3 3 0 1 0 6 0a3 3 0 1 0-6 0', color: '#6366f1', order: 0 },
|
||||
instructions: { label: 'Instructions', icon: 'M4 19.5A2.5 2.5 0 0 1 6.5 17H20|M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z', color: '#3b82f6', order: 1 },
|
||||
plugins: { label: 'Plugins', icon: 'M12 2L2 7l10 5 10-5-10-5z|M2 17l10 5 10-5|M2 12l10 5 10-5', color: '#8b5cf6', order: 2 },
|
||||
history: { label: 'History', icon: 'M12 8v4l3 3|M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z', color: '#f59e0b', order: 3 },
|
||||
debug: { label: 'Debug logs', icon: 'M12 12m-1 0a1 1 0 1 0 2 0 1 1 0 0 0-2 0|M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z', color: '#ef4444', order: 4 },
|
||||
cache: { label: 'Cache', icon: 'M22 12H2|M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z', color: '#06b6d4', order: 5 },
|
||||
sessions: { label: 'Session data', icon: 'M4 17l6-6 4 4 6-6|M4 17h16', color: '#10b981', order: 6 },
|
||||
backups: { label: 'Backups', icon: 'M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8|M21 3v5h-5', color: '#9ca3af', order: 7 },
|
||||
other: { label: 'Other files', icon: 'M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z|M14 2v6h6', color: '#78716c', order: 8 }
|
||||
}
|
||||
|
||||
export { CATEGORY_META }
|
||||
export type { FileCategory, AgentFile, Agent }
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
export { formatSize }
|
||||
|
||||
export const useAgentsStore = defineStore('agents', () => {
|
||||
const agents = ref<Agent[]>([])
|
||||
const selectedAgentId = ref<string | null>(null)
|
||||
const openFile = ref<OpenFile | null>(null)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const collapsedCategories = ref<Set<string>>(new Set())
|
||||
|
||||
const isDirty = computed(() => {
|
||||
if (!openFile.value) return false
|
||||
return openFile.value.content !== openFile.value.originalContent
|
||||
})
|
||||
|
||||
const selectedAgent = computed(() => {
|
||||
if (!selectedAgentId.value) return null
|
||||
return agents.value.find(a => a.id === selectedAgentId.value) || null
|
||||
})
|
||||
|
||||
const groupedFiles = computed((): CategoryInfo[] => {
|
||||
const agent = selectedAgent.value
|
||||
if (!agent) return []
|
||||
|
||||
const groups = new Map<FileCategory, AgentFile[]>()
|
||||
for (const file of agent.files) {
|
||||
const cat = file.category
|
||||
if (!groups.has(cat)) groups.set(cat, [])
|
||||
groups.get(cat)!.push(file)
|
||||
}
|
||||
|
||||
return Array.from(groups.entries())
|
||||
.map(([key, files]) => ({
|
||||
key,
|
||||
label: CATEGORY_META[key].label,
|
||||
icon: CATEGORY_META[key].icon,
|
||||
color: CATEGORY_META[key].color,
|
||||
files
|
||||
}))
|
||||
.sort((a, b) => CATEGORY_META[a.key].order - CATEGORY_META[b.key].order)
|
||||
})
|
||||
|
||||
async function fetchAgents() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch('/api/agents')
|
||||
if (!res.ok) throw new Error('Failed to fetch agents')
|
||||
agents.value = await res.json()
|
||||
if (agents.value.length && !selectedAgentId.value) {
|
||||
selectedAgentId.value = agents.value[0].id
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectAgent(id: string) {
|
||||
selectedAgentId.value = id
|
||||
openFile.value = null
|
||||
}
|
||||
|
||||
async function loadFile(agentId: string, file: AgentFile) {
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch(`/api/agents/file?path=${encodeURIComponent(file.path)}`)
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.error || 'Failed to load file')
|
||||
}
|
||||
const data = await res.json()
|
||||
openFile.value = {
|
||||
path: file.path,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
content: data.content,
|
||||
originalContent: data.content,
|
||||
agentId,
|
||||
category: file.category
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFile() {
|
||||
if (!openFile.value || !isDirty.value) return
|
||||
saving.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const res = await fetch('/api/agents/file', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
path: openFile.value.path,
|
||||
content: openFile.value.content
|
||||
})
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json()
|
||||
throw new Error(data.error || 'Failed to save file')
|
||||
}
|
||||
openFile.value.originalContent = openFile.value.content
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function revertFile() {
|
||||
if (!openFile.value) return
|
||||
openFile.value.content = openFile.value.originalContent
|
||||
}
|
||||
|
||||
function toggleCategory(key: string) {
|
||||
if (collapsedCategories.value.has(key)) {
|
||||
collapsedCategories.value.delete(key)
|
||||
} else {
|
||||
collapsedCategories.value.add(key)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
agents,
|
||||
selectedAgentId,
|
||||
selectedAgent,
|
||||
openFile,
|
||||
loading,
|
||||
saving,
|
||||
error,
|
||||
collapsedCategories,
|
||||
isDirty,
|
||||
groupedFiles,
|
||||
fetchAgents,
|
||||
selectAgent,
|
||||
loadFile,
|
||||
saveFile,
|
||||
revertFile,
|
||||
toggleCategory
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user