feat: refactor names
This commit is contained in:
330
frontend/src/app/kosmos/KosmosContext.tsx
Normal file
330
frontend/src/app/kosmos/KosmosContext.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* Platform context: projects list and handlers for create/delete/rename/updateLastEdited/reorder/restore.
|
||||
* Used by AppSidebar, ProjectsTablePage, and canvas route.
|
||||
*/
|
||||
|
||||
import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'
|
||||
import type { Project } from './types'
|
||||
import { saveGraphToStorage } from '../pleroma/projectGraphStorage'
|
||||
import { PROJECT_VERSION } from '../pleroma/projectGraphStorage'
|
||||
|
||||
const STORAGE_KEY = 'zui_platform_projects'
|
||||
const ORDER_STORAGE_KEY = 'zui_platform_project_order'
|
||||
const RECENT_STORAGE_KEY = 'zui_platform_recent_project_ids'
|
||||
const CANVAS_MINIMAP_KEY = 'zui_canvas_show_minimap'
|
||||
const AI_CONNECTION_KEY = 'zui_ai_connection'
|
||||
const RECENT_MAX = 5
|
||||
|
||||
export type AiConnectionProvider = 'openai' | 'local'
|
||||
|
||||
export type AiConnection = {
|
||||
provider: AiConnectionProvider
|
||||
baseURL: string
|
||||
model: string
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
const DEFAULT_AI_CONNECTION: AiConnection = {
|
||||
provider: 'local',
|
||||
baseURL: 'http://localhost:1234/v1',
|
||||
model: 'local-model',
|
||||
apiKey: '',
|
||||
}
|
||||
|
||||
function loadAiConnection(): AiConnection {
|
||||
try {
|
||||
const raw = localStorage.getItem(AI_CONNECTION_KEY)
|
||||
if (!raw) return DEFAULT_AI_CONNECTION
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
if (!parsed || typeof parsed !== 'object') return DEFAULT_AI_CONNECTION
|
||||
const p = parsed as Record<string, unknown>
|
||||
return {
|
||||
provider: p.provider === 'openai' ? 'openai' : 'local',
|
||||
baseURL: typeof p.baseURL === 'string' ? p.baseURL : DEFAULT_AI_CONNECTION.baseURL,
|
||||
model: typeof p.model === 'string' ? p.model : DEFAULT_AI_CONNECTION.model,
|
||||
apiKey: typeof p.apiKey === 'string' ? p.apiKey : '',
|
||||
}
|
||||
} catch {
|
||||
return DEFAULT_AI_CONNECTION
|
||||
}
|
||||
}
|
||||
|
||||
function saveAiConnection(value: AiConnection) {
|
||||
try {
|
||||
localStorage.setItem(AI_CONNECTION_KEY, JSON.stringify(value))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function loadShowMinimap(): boolean {
|
||||
try {
|
||||
const raw = localStorage.getItem(CANVAS_MINIMAP_KEY)
|
||||
if (raw === 'true') return true
|
||||
if (raw === 'false') return false
|
||||
} catch {}
|
||||
return false
|
||||
}
|
||||
|
||||
function saveShowMinimap(value: boolean) {
|
||||
try {
|
||||
localStorage.setItem(CANVAS_MINIMAP_KEY, JSON.stringify(value))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function loadOrder(): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(ORDER_STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveOrder(ids: string[]) {
|
||||
localStorage.setItem(ORDER_STORAGE_KEY, JSON.stringify(ids))
|
||||
}
|
||||
|
||||
function loadRecentIds(): string[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(RECENT_STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string').slice(0, RECENT_MAX) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveRecentIds(ids: string[]) {
|
||||
localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(ids))
|
||||
}
|
||||
|
||||
function loadProjects(): Project[] {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return []
|
||||
const parsed = JSON.parse(raw) as unknown
|
||||
if (!Array.isArray(parsed)) return []
|
||||
return parsed
|
||||
.filter(
|
||||
(p): p is Project =>
|
||||
p &&
|
||||
typeof p === 'object' &&
|
||||
typeof (p as Project).id === 'string' &&
|
||||
typeof (p as Project).name === 'string' &&
|
||||
typeof (p as Project).iconId === 'string' &&
|
||||
typeof (p as Project).createdAt === 'number'
|
||||
)
|
||||
.map((p) => ({
|
||||
...p,
|
||||
lastEditedAt: typeof (p as Project).lastEditedAt === 'number' ? (p as Project).lastEditedAt : p.createdAt,
|
||||
}))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function saveProjects(projects: Project[]) {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(projects))
|
||||
}
|
||||
|
||||
export type GraphSnapshot = { nodes: unknown[]; edges: unknown[] }
|
||||
|
||||
/** Projects sorted by projectOrder, then by lastEditedAt desc for any not in order */
|
||||
export function sortProjectsByOrder(projects: Project[], order: string[]): Project[] {
|
||||
const byId = new Map(projects.map((p) => [p.id, p]))
|
||||
const ordered: Project[] = []
|
||||
const seen = new Set<string>()
|
||||
for (const id of order) {
|
||||
const p = byId.get(id)
|
||||
if (p) {
|
||||
ordered.push(p)
|
||||
seen.add(id)
|
||||
}
|
||||
}
|
||||
const rest = projects
|
||||
.filter((p) => !seen.has(p.id))
|
||||
.sort((a, b) => (b.lastEditedAt ?? b.createdAt) - (a.lastEditedAt ?? a.createdAt))
|
||||
return [...ordered, ...rest]
|
||||
}
|
||||
|
||||
export type KosmosContextValue = {
|
||||
projects: Project[]
|
||||
projectOrder: string[]
|
||||
/** Projects in display order (sidebar order, then lastEditedAt) */
|
||||
orderedProjects: Project[]
|
||||
/** Last RECENT_MAX accessed project IDs (most recent first) */
|
||||
recentProjectIds: string[]
|
||||
recordProjectAccess: (id: string) => void
|
||||
persist: (next: Project[]) => void
|
||||
createProject: (project: Project) => void
|
||||
deleteProject: (id: string) => void
|
||||
renameProject: (id: string, name: string) => void
|
||||
updateLastEdited: (id: string) => void
|
||||
reorderProjects: (orderedIds: string[]) => void
|
||||
restoreProject: (project: Project, graphSnapshot: GraphSnapshot | null) => void
|
||||
/** Canvas: show React Flow minimap (persisted) */
|
||||
showMinimap: boolean
|
||||
setShowMinimap: (value: boolean) => void
|
||||
/** Agent node: AI connection (persisted). Sent to backend when running agent. */
|
||||
aiConnection: AiConnection
|
||||
setAiConnection: (value: AiConnection) => void
|
||||
}
|
||||
|
||||
const KosmosContext = createContext<KosmosContextValue | null>(null)
|
||||
|
||||
export function KosmosProvider({ children }: { children: React.ReactNode }) {
|
||||
const [projects, setProjects] = useState<Project[]>(loadProjects)
|
||||
const [projectOrder, setProjectOrder] = useState<string[]>(loadOrder)
|
||||
const [recentProjectIds, setRecentProjectIds] = useState<string[]>(loadRecentIds)
|
||||
const [showMinimap, setShowMinimapState] = useState<boolean>(loadShowMinimap)
|
||||
const [aiConnection, setAiConnectionState] = useState<AiConnection>(loadAiConnection)
|
||||
|
||||
const setShowMinimap = useCallback((value: boolean) => {
|
||||
setShowMinimapState(value)
|
||||
saveShowMinimap(value)
|
||||
}, [])
|
||||
|
||||
const setAiConnection = useCallback((value: AiConnection) => {
|
||||
setAiConnectionState(value)
|
||||
saveAiConnection(value)
|
||||
}, [])
|
||||
|
||||
const persist = useCallback((next: Project[]) => {
|
||||
setProjects(next)
|
||||
saveProjects(next)
|
||||
}, [])
|
||||
|
||||
const createProject = useCallback(
|
||||
(project: Project) => {
|
||||
const withEdited = { ...project, lastEditedAt: project.createdAt }
|
||||
persist([...projects, withEdited])
|
||||
setProjectOrder((prev) => {
|
||||
const next = prev.includes(project.id) ? prev : [project.id, ...prev]
|
||||
saveOrder(next)
|
||||
return next
|
||||
})
|
||||
},
|
||||
[projects, persist]
|
||||
)
|
||||
|
||||
const deleteProject = useCallback(
|
||||
(id: string) => {
|
||||
const next = projects.filter((p) => p.id !== id)
|
||||
persist(next)
|
||||
setProjectOrder((prev) => {
|
||||
const nextOrder = prev.filter((oid) => oid !== id)
|
||||
saveOrder(nextOrder)
|
||||
return nextOrder
|
||||
})
|
||||
setRecentProjectIds((prev) => {
|
||||
const next = prev.filter((oid) => oid !== id)
|
||||
saveRecentIds(next)
|
||||
return next
|
||||
})
|
||||
},
|
||||
[projects, persist]
|
||||
)
|
||||
|
||||
const renameProject = useCallback(
|
||||
(id: string, name: string) => {
|
||||
const next = projects.map((p) => (p.id === id ? { ...p, name } : p))
|
||||
persist(next)
|
||||
},
|
||||
[projects, persist]
|
||||
)
|
||||
|
||||
const updateLastEdited = useCallback(
|
||||
(id: string) => {
|
||||
const now = Date.now()
|
||||
const next = projects.map((p) => (p.id === id ? { ...p, lastEditedAt: now } : p))
|
||||
persist(next)
|
||||
},
|
||||
[projects, persist]
|
||||
)
|
||||
|
||||
const reorderProjects = useCallback((orderedIds: string[]) => {
|
||||
setProjectOrder(orderedIds)
|
||||
saveOrder(orderedIds)
|
||||
}, [])
|
||||
|
||||
const recordProjectAccess = useCallback((id: string) => {
|
||||
setRecentProjectIds((prev) => {
|
||||
const next = [id, ...prev.filter((x) => x !== id)].slice(0, RECENT_MAX)
|
||||
saveRecentIds(next)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const restoreProject = useCallback(
|
||||
(project: Project, graphSnapshot: GraphSnapshot | null) => {
|
||||
persist([...projects, project])
|
||||
setProjectOrder((prev) => {
|
||||
const next = prev.includes(project.id) ? prev : [project.id, ...prev]
|
||||
saveOrder(next)
|
||||
return next
|
||||
})
|
||||
if (graphSnapshot) {
|
||||
saveGraphToStorage(project.id, {
|
||||
version: PROJECT_VERSION,
|
||||
nodes: graphSnapshot.nodes,
|
||||
edges: graphSnapshot.edges,
|
||||
})
|
||||
}
|
||||
},
|
||||
[projects, persist]
|
||||
)
|
||||
|
||||
const orderedProjects = useMemo(
|
||||
() => sortProjectsByOrder(projects, projectOrder),
|
||||
[projects, projectOrder]
|
||||
)
|
||||
|
||||
const value: KosmosContextValue = useMemo(
|
||||
() => ({
|
||||
projects,
|
||||
projectOrder,
|
||||
orderedProjects,
|
||||
recentProjectIds,
|
||||
recordProjectAccess,
|
||||
persist,
|
||||
createProject,
|
||||
deleteProject,
|
||||
renameProject,
|
||||
updateLastEdited,
|
||||
reorderProjects,
|
||||
restoreProject,
|
||||
showMinimap,
|
||||
setShowMinimap,
|
||||
aiConnection,
|
||||
setAiConnection,
|
||||
}),
|
||||
[
|
||||
projects,
|
||||
projectOrder,
|
||||
orderedProjects,
|
||||
recentProjectIds,
|
||||
recordProjectAccess,
|
||||
persist,
|
||||
createProject,
|
||||
deleteProject,
|
||||
renameProject,
|
||||
updateLastEdited,
|
||||
reorderProjects,
|
||||
restoreProject,
|
||||
showMinimap,
|
||||
setShowMinimap,
|
||||
aiConnection,
|
||||
setAiConnection,
|
||||
]
|
||||
)
|
||||
|
||||
return <KosmosContext.Provider value={value}>{children}</KosmosContext.Provider>
|
||||
}
|
||||
|
||||
export function usePlatform() {
|
||||
const ctx = useContext(KosmosContext)
|
||||
if (!ctx) throw new Error('usePlatform must be used within PlatformProvider')
|
||||
return ctx
|
||||
}
|
||||
Reference in New Issue
Block a user