Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
commit
03634b1ca3
11340 files changed
+3442369
No files matched your search
@@ -0,0 +1,219 @@
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import { ArchiveSkillConfirmDialog, fireOptimistic } from '@/app/learning/archive-skill-confirm-dialog'
|
||||
import { CodeEditor } from '@/components/chat/code-editor'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { deleteLearningNode, editLearningNode, getLearningNode } from '@/hermes'
|
||||
import { notifyError } from '@/store/notifications'
|
||||
import { evictStarmapNode, loadStarmapGraph } from '@/store/starmap'
|
||||
|
||||
import { useOnProfileSwitch } from '../hooks/use-on-profile-switch'
|
||||
|
||||
export interface NodeMenuTarget {
|
||||
id: string
|
||||
kind: 'memory' | 'skill'
|
||||
label: string
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface NodeContextMenuProps {
|
||||
onClose: () => void
|
||||
onNodeRemoved: () => void
|
||||
target: NodeMenuTarget | null
|
||||
}
|
||||
|
||||
interface EditState {
|
||||
content: string
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
/** Right-click actions for a star-map node: edit (modal) or delete (confirm). */
|
||||
export function NodeContextMenu({ onClose, onNodeRemoved, target }: NodeContextMenuProps) {
|
||||
const [editing, setEditing] = useState<EditState | null>(null)
|
||||
const [deleting, setDeleting] = useState<Omit<NodeMenuTarget, 'x' | 'y'> | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [error, setError] = useState<null | string>(null)
|
||||
|
||||
// Bumped on profile switch so an in-flight openEdit fetch from profile A can't
|
||||
// reopen the editor with A's node content after switching to B.
|
||||
const editEpoch = useRef(0)
|
||||
|
||||
// A profile switch swaps the backend under an open edit/delete dialog — its
|
||||
// node id belongs to the previous profile, so a Save/Delete after the switch
|
||||
// would hit the newly active profile. Close everything on switch.
|
||||
useOnProfileSwitch(() => {
|
||||
editEpoch.current += 1
|
||||
setEditing(null)
|
||||
setDeleting(null)
|
||||
setError(null)
|
||||
})
|
||||
|
||||
const noun = target?.kind === 'memory' ? 'memory' : 'skill'
|
||||
|
||||
const openEdit = async () => {
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
const epoch = editEpoch.current
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const detail = await getLearningNode(target.id)
|
||||
|
||||
if (editEpoch.current !== epoch) {
|
||||
return
|
||||
}
|
||||
|
||||
setEditing({ content: detail.content, id: target.id, label: target.label })
|
||||
onClose()
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
if (!editing) {
|
||||
return
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const res = await editLearningNode(editing.id, editing.content)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.message)
|
||||
}
|
||||
|
||||
setEditing(null)
|
||||
void loadStarmapGraph(true)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const menuOpen = target && !editing && !deleting
|
||||
|
||||
return (
|
||||
<>
|
||||
{menuOpen ? (
|
||||
<>
|
||||
<div className="fixed inset-0 z-50" onClick={onClose} onContextMenu={e => e.preventDefault()} />
|
||||
{/* Styled to DropdownMenuContent/Item scale (rounded-lg card, p-1,
|
||||
text-xs rows) — the hand-rolled fixed positioning stays because
|
||||
the target is a canvas point, not a DOM anchor. */}
|
||||
<div
|
||||
className="fixed z-50 min-w-36 rounded-lg border border-(--ui-stroke-secondary) bg-[color-mix(in_srgb,var(--ui-bg-elevated)_96%,transparent)] p-1 shadow-md backdrop-blur-md"
|
||||
style={{ left: target.x, top: target.y }}
|
||||
>
|
||||
<div className="truncate px-2 py-1 text-[0.68rem] text-muted-foreground">{target.label}</div>
|
||||
<button
|
||||
className="block w-full cursor-pointer rounded-md px-2 py-1 text-left text-xs hover:bg-(--ui-control-active-background) hover:text-foreground disabled:opacity-50"
|
||||
disabled={loading}
|
||||
onClick={() => void openEdit()}
|
||||
type="button"
|
||||
>
|
||||
Edit {noun}…
|
||||
</button>
|
||||
<button
|
||||
className="block w-full cursor-pointer rounded-md px-2 py-1 text-left text-xs text-destructive hover:bg-destructive/10"
|
||||
onClick={() => {
|
||||
setDeleting({ id: target.id, kind: target.kind, label: target.label })
|
||||
onClose()
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{target.kind === 'skill' ? 'Archive skill' : 'Delete memory'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<Dialog onOpenChange={value => !value && !saving && setEditing(null)} open={Boolean(editing)}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit {editing?.label}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="h-80">
|
||||
{editing && (
|
||||
<CodeEditor
|
||||
filePath={noun === 'skill' ? 'SKILL.md' : 'memory.md'}
|
||||
framed
|
||||
initialValue={editing.content}
|
||||
key={editing.id}
|
||||
onCancel={() => !saving && setEditing(null)}
|
||||
onChange={content => setEditing(prev => (prev ? { ...prev, content } : prev))}
|
||||
onSave={() => void save()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
||||
<DialogFooter>
|
||||
<Button disabled={saving} onClick={() => setEditing(null)} type="button" variant="ghost">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button disabled={saving} onClick={() => void save()}>
|
||||
{saving ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{deleting?.kind === 'skill' ? (
|
||||
<ArchiveSkillConfirmDialog
|
||||
onApply={() => {
|
||||
onNodeRemoved()
|
||||
|
||||
return evictStarmapNode(deleting.id)
|
||||
}}
|
||||
onClose={() => setDeleting(null)}
|
||||
onFailure={(err, name) => notifyError(err, name)}
|
||||
open
|
||||
skillId={deleting.id}
|
||||
skillName={deleting.label}
|
||||
/>
|
||||
) : (
|
||||
<ConfirmDialog
|
||||
confirmLabel="Delete"
|
||||
description="This memory is removed permanently."
|
||||
destructive
|
||||
dismissOnConfirm
|
||||
onClose={() => setDeleting(null)}
|
||||
onConfirm={() => {
|
||||
if (!deleting) {
|
||||
return
|
||||
}
|
||||
|
||||
const { id, label } = deleting
|
||||
const rollback = evictStarmapNode(id)
|
||||
onNodeRemoved()
|
||||
|
||||
fireOptimistic(
|
||||
deleteLearningNode(id).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(res.message)
|
||||
}
|
||||
}),
|
||||
rollback,
|
||||
err => notifyError(err, label)
|
||||
)
|
||||
}}
|
||||
open={Boolean(deleting)}
|
||||
title={`Delete ${deleting?.label ?? ''}?`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user