diff --git a/src/agentkit/server/app.py b/src/agentkit/server/app.py
index eabfb14..133131b 100644
--- a/src/agentkit/server/app.py
+++ b/src/agentkit/server/app.py
@@ -680,6 +680,7 @@ def create_app(
if gui_mode:
from pathlib import Path as _Path
from fastapi.responses import HTMLResponse, FileResponse
+ from fastapi.staticfiles import StaticFiles
_static_dir = _Path(__file__).parent / "static"
@@ -691,4 +692,27 @@ def create_app(
return FileResponse(str(index_path))
return HTMLResponse("
AgentKit GUI not found
", status_code=404)
+ # SPA fallback: serve index.html for all non-API, non-static routes
+ @app.get("/{path:path}", response_class=HTMLResponse, include_in_schema=False)
+ async def spa_fallback(path: str):
+ """Serve index.html for SPA client-side routing."""
+ # Don't intercept API routes
+ if path.startswith("api/"):
+ return HTMLResponse("Not Found
", status_code=404)
+ # Try to serve a real static file first
+ file_path = _static_dir / path
+ if file_path.is_file():
+ return FileResponse(str(file_path))
+ # Fallback to index.html for SPA routing
+ index_path = _static_dir / "index.html"
+ if index_path.exists():
+ return FileResponse(str(index_path))
+ return HTMLResponse("Not Found
", status_code=404)
+
+ # Mount static assets last (js, css, images, etc.)
+ # mount() is checked after route matching, so API routes take priority
+ assets_dir = _static_dir / "assets"
+ if assets_dir.exists():
+ app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="static-assets")
+
return app
diff --git a/src/agentkit/server/frontend/src/api/kb.ts b/src/agentkit/server/frontend/src/api/kb.ts
index 82319d4..1b70373 100644
--- a/src/agentkit/server/frontend/src/api/kb.ts
+++ b/src/agentkit/server/frontend/src/api/kb.ts
@@ -11,6 +11,7 @@ export interface IKbSource {
status: string
document_count: number
last_synced: string | null
+ config?: Record
}
export interface IAddSourceRequest {
diff --git a/src/agentkit/server/frontend/src/api/skills.ts b/src/agentkit/server/frontend/src/api/skills.ts
index d34794b..d3bdbbb 100644
--- a/src/agentkit/server/frontend/src/api/skills.ts
+++ b/src/agentkit/server/frontend/src/api/skills.ts
@@ -71,6 +71,36 @@ class SkillsApiClient extends BaseApiClient {
return this.request<{ capabilities: ICapabilityInfo[] }>('/capabilities')
}
+ /** Install a skill by name (searches GitHub) or from a source URL */
+ async installSkill(
+ name: string,
+ source?: string
+ ): Promise<{ status: string; name: string; path: string }> {
+ // The install endpoint is on /api/v1/skills/install, not under skill-management
+ const url = `/api/v1/skills/install`
+ const resp = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name, source }),
+ })
+ if (!resp.ok) {
+ const err = await resp.json().catch(() => ({ detail: resp.statusText }))
+ throw new Error(err.detail ?? resp.statusText)
+ }
+ return resp.json()
+ }
+
+ /** Uninstall a skill */
+ async uninstallSkill(skillName: string): Promise<{ status: string; name: string }> {
+ const url = `/api/v1/skills/${encodeURIComponent(skillName)}`
+ const resp = await fetch(url, { method: 'DELETE' })
+ if (!resp.ok) {
+ const err = await resp.json().catch(() => ({ detail: resp.statusText }))
+ throw new Error(err.detail ?? resp.statusText)
+ }
+ return resp.json()
+ }
+
/** Reload a skill */
async reloadSkill(
skillName: string
diff --git a/src/agentkit/server/frontend/src/components/evolution/ExperienceTimeline.vue b/src/agentkit/server/frontend/src/components/evolution/ExperienceTimeline.vue
index 6139cec..b1270cc 100644
--- a/src/agentkit/server/frontend/src/components/evolution/ExperienceTimeline.vue
+++ b/src/agentkit/server/frontend/src/components/evolution/ExperienceTimeline.vue
@@ -61,11 +61,11 @@
diff --git a/src/agentkit/server/frontend/src/components/workflow/FlowCanvas.vue b/src/agentkit/server/frontend/src/components/workflow/FlowCanvas.vue
index 5d954c9..6a758d4 100644
--- a/src/agentkit/server/frontend/src/components/workflow/FlowCanvas.vue
+++ b/src/agentkit/server/frontend/src/components/workflow/FlowCanvas.vue
@@ -21,8 +21,8 @@
()
@@ -69,15 +67,13 @@ const emit = defineEmits<{
save: []
execute: []
clear: []
- 'update:nodes': [nodes: any[]]
- 'update:edges': [edges: any[]]
'node-select': [nodeId: string | null]
'node-drop': [nodeType: string, position: { x: number; y: number }]
}>()
const canvasRef = ref()
-const nodeTypes = {
+const nodeTypes: Record = {
skill: markRaw(SkillNode),
condition: markRaw(ConditionNode),
approval: markRaw(ApprovalNode),
@@ -164,17 +160,17 @@ onUnmounted(() => {
function onValidate() {
// Basic validation
- if (props.nodes.length === 0) {
+ if (store.flowNodes.length === 0) {
message.warning('工作流为空,请添加节点')
return
}
// Check for nodes without connections (except the first)
- const connectedSources = new Set(props.edges.map((e: any) => e.source))
- const connectedTargets = new Set(props.edges.map((e: any) => e.target))
+ const connectedSources = new Set(store.flowEdges.map((e: any) => e.source))
+ const connectedTargets = new Set(store.flowEdges.map((e: any) => e.target))
const allConnected = new Set([...connectedSources, ...connectedTargets])
- const orphanNodes = props.nodes.filter((n: any) => !allConnected.has(n.id))
- if (orphanNodes.length > 0 && props.nodes.length > 1) {
+ const orphanNodes = store.flowNodes.filter((n: any) => !allConnected.has(n.id))
+ if (orphanNodes.length > 0 && store.flowNodes.length > 1) {
message.warning(`存在未连接的节点: ${orphanNodes.map((n: any) => n.data?.label).join(', ')}`)
return
}
diff --git a/src/agentkit/server/frontend/src/stores/workflow.ts b/src/agentkit/server/frontend/src/stores/workflow.ts
index 55a821a..16e5d64 100644
--- a/src/agentkit/server/frontend/src/stores/workflow.ts
+++ b/src/agentkit/server/frontend/src/stores/workflow.ts
@@ -30,9 +30,9 @@ export const useWorkflowStore = defineStore('workflow', () => {
const isLoading = ref(false)
const error = ref(null)
- // Vue Flow state
- const flowNodes = ref[]>([])
- const flowEdges = ref([])
+ // Vue Flow state — use any[] to avoid @vue-flow/core deep type recursion with Vue 3.5+
+ const flowNodes = ref([])
+ const flowEdges = ref([])
const selectedNodeId = ref(null)
// Undo/Redo state
@@ -51,9 +51,9 @@ export const useWorkflowStore = defineStore('workflow', () => {
const executionHistoryTotal = ref(0)
// --- Getters ---
- const selectedNode = computed | null>(() => {
+ const selectedNode = computed(() => {
if (!selectedNodeId.value) return null
- return flowNodes.value.find((n) => n.id === selectedNodeId.value) || null
+ return flowNodes.value.find((n: any) => n.id === selectedNodeId.value) || null
})
const selectedNodeData = computed(() => {
@@ -65,12 +65,12 @@ export const useWorkflowStore = defineStore('workflow', () => {
// --- Internal mutation methods (no command tracking) ---
- function _addNodeDirect(node: Node): void {
- flowNodes.value = [...flowNodes.value, node]
+ function _addNodeDirect(node: any): void {
+ flowNodes.value.push(node)
}
- function _removeNodeDirect(nodeId: string): { node: Node; edges: Edge[] } | null {
- const node = flowNodes.value.find((n) => n.id === nodeId)
+ function _removeNodeDirect(nodeId: string): { node: any; edges: any[] } | null {
+ const node = flowNodes.value.find((n: any) => n.id === nodeId)
if (!node) return null
const removedEdges = flowEdges.value.filter(
(e) => e.source === nodeId || e.target === nodeId
@@ -97,11 +97,12 @@ export const useWorkflowStore = defineStore('workflow', () => {
}
function _updateNodeDataDirect(nodeId: string, data: Partial): void {
- const index = flowNodes.value.findIndex((n) => n.id === nodeId)
+ const index = flowNodes.value.findIndex((n: any) => n.id === nodeId)
if (index !== -1) {
+ const existing = flowNodes.value[index]
flowNodes.value[index] = {
- ...flowNodes.value[index],
- data: { ...flowNodes.value[index].data, ...data },
+ ...existing,
+ data: { ...existing.data, ...data },
}
}
}
diff --git a/src/agentkit/server/frontend/src/styles/index.ts b/src/agentkit/server/frontend/src/styles/index.ts
index b063da7..ca09a45 100644
--- a/src/agentkit/server/frontend/src/styles/index.ts
+++ b/src/agentkit/server/frontend/src/styles/index.ts
@@ -5,4 +5,6 @@
*/
import './tokens.css'
+import './transitions.css'
+import './responsive.css'
export { themeConfig } from './theme'
diff --git a/src/agentkit/server/frontend/src/styles/responsive.css b/src/agentkit/server/frontend/src/styles/responsive.css
new file mode 100644
index 0000000..be282f2
--- /dev/null
+++ b/src/agentkit/server/frontend/src/styles/responsive.css
@@ -0,0 +1,89 @@
+/**
+ * Fischer AgentKit Responsive Breakpoints
+ *
+ * ≥1440px: Four quadrants fully visible
+ * 1280-1440px: Bottom-right quadrant auto-collapsed
+ * <1280px: Prompt to use larger screen
+ */
+
+/* ── Full four-quadrant layout ── */
+@media (min-width: 1440px) {
+ .agent-layout__body {
+ display: flex;
+ }
+}
+
+/* ── Compact: bottom-right quadrant collapsed ── */
+@media (min-width: 1280px) and (max-width: 1439px) {
+ .agent-layout__body {
+ display: flex;
+ }
+
+ /* Auto-collapse bottom-right quadrant via CSS */
+ .quadrant-panel--bottom-right-collapsed {
+ height: auto !important;
+ }
+}
+
+/* ── Too small: show prompt ── */
+@media (max-width: 1279px) {
+ .agent-layout__body {
+ display: none;
+ }
+
+ .agent-layout__small-screen {
+ display: flex;
+ }
+}
+
+/* ── Default: hide small screen prompt ── */
+.agent-layout__small-screen {
+ display: none;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ gap: var(--space-4);
+ color: var(--text-tertiary);
+ text-align: center;
+ padding: var(--space-8);
+}
+
+.agent-layout__small-screen h2 {
+ font-size: var(--font-lg);
+ color: var(--text-primary);
+}
+
+.agent-layout__small-screen p {
+ font-size: var(--font-base);
+ max-width: 400px;
+}
+
+/* ── Quadrant min-size for readability ── */
+.quadrant-panel {
+ min-width: var(--quadrant-min-size);
+ min-height: var(--quadrant-min-size);
+}
+
+/* ── TopNav responsive ── */
+@media (max-width: 768px) {
+ .top-nav__center {
+ display: none;
+ }
+}
+
+/* ── Chat sidebar responsive ── */
+@media (max-width: 1024px) {
+ .chat-sidebar:not(.chat-sidebar--collapsed) {
+ width: 200px;
+ }
+}
+
+/* ── Print: hide interactive elements ── */
+@media print {
+ .top-nav,
+ .split-pane__handle,
+ .quadrant-panel__collapse-btn {
+ display: none !important;
+ }
+}
diff --git a/src/agentkit/server/frontend/src/styles/transitions.css b/src/agentkit/server/frontend/src/styles/transitions.css
new file mode 100644
index 0000000..7d8eb20
--- /dev/null
+++ b/src/agentkit/server/frontend/src/styles/transitions.css
@@ -0,0 +1,132 @@
+/**
+ * Fischer AgentKit Transition Animations
+ *
+ * Unified transition classes for Vue components.
+ * All durations reference Design Token variables for consistency.
+ */
+
+/* ── Fade ── */
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity var(--transition-fast);
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
+/* ── Slide Up ── */
+.slide-up-enter-active,
+.slide-up-leave-active {
+ transition: transform var(--transition-normal), opacity var(--transition-fast);
+}
+
+.slide-up-enter-from,
+.slide-up-leave-to {
+ transform: translateY(8px);
+ opacity: 0;
+}
+
+/* ── Slide Down ── */
+.slide-down-enter-active,
+.slide-down-leave-active {
+ transition: transform var(--transition-normal), opacity var(--transition-fast);
+}
+
+.slide-down-enter-from,
+.slide-down-leave-to {
+ transform: translateY(-8px);
+ opacity: 0;
+}
+
+/* ── Slide Right ── */
+.slide-right-enter-active,
+.slide-right-leave-active {
+ transition: transform var(--transition-normal), opacity var(--transition-fast);
+}
+
+.slide-right-enter-from,
+.slide-right-leave-to {
+ transform: translateX(-8px);
+ opacity: 0;
+}
+
+/* ── Collapse (height) ── */
+.collapse-enter-active,
+.collapse-leave-active {
+ transition: max-height var(--transition-slow) ease, opacity var(--transition-fast);
+ overflow: hidden;
+}
+
+.collapse-enter-from,
+.collapse-leave-to {
+ max-height: 0;
+ opacity: 0;
+}
+
+.collapse-enter-to,
+.collapse-leave-from {
+ max-height: 500px;
+ opacity: 1;
+}
+
+/* ── Scale ── */
+.scale-enter-active,
+.scale-leave-active {
+ transition: transform var(--transition-fast), opacity var(--transition-fast);
+}
+
+.scale-enter-from,
+.scale-leave-to {
+ transform: scale(0.95);
+ opacity: 0;
+}
+
+/* ── Stagger list items ── */
+.stagger-list-enter-active {
+ transition: transform var(--transition-normal), opacity var(--transition-fast);
+}
+
+.stagger-list-leave-active {
+ transition: transform var(--transition-fast), opacity var(--transition-fast);
+}
+
+.stagger-list-enter-from,
+.stagger-list-leave-to {
+ transform: translateY(8px);
+ opacity: 0;
+}
+
+.stagger-list-move {
+ transition: transform var(--transition-normal);
+}
+
+/* ── Skeleton pulse ── */
+@keyframes skeleton-pulse {
+ 0% { opacity: 1; }
+ 50% { opacity: 0.4; }
+ 100% { opacity: 1; }
+}
+
+.skeleton-loading {
+ animation: skeleton-pulse 1.5s ease-in-out infinite;
+ background: linear-gradient(
+ 90deg,
+ var(--bg-tertiary) 25%,
+ var(--border-color) 50%,
+ var(--bg-tertiary) 75%
+ );
+ background-size: 200% 100%;
+ border-radius: var(--radius-sm);
+}
+
+/* ── Pulse dot (running status) ── */
+@keyframes pulse-dot {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.3; }
+}
+
+.pulse-dot {
+ animation: pulse-dot 1.5s ease-in-out infinite;
+}
diff --git a/src/agentkit/server/static/index.html b/src/agentkit/server/static/index.html
index cf42de2..1cfb518 100644
--- a/src/agentkit/server/static/index.html
+++ b/src/agentkit/server/static/index.html
@@ -1,909 +1,14 @@
-
-
-
-AgentKit
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
🤖
-
欢迎使用 AgentKit
-
开始一段新对话,或从侧边栏选择已有会话。
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Fischer AgentKit
+
+
+
+
+
+