cognits 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cognits-0.0.1/.gitignore +30 -0
- cognits-0.0.1/LICENSE +21 -0
- cognits-0.0.1/PKG-INFO +54 -0
- cognits-0.0.1/README.md +38 -0
- cognits-0.0.1/frontend/src/App.tsx +180 -0
- cognits-0.0.1/frontend/src/FileTree.tsx +101 -0
- cognits-0.0.1/frontend/src/FileTreeWrapper.tsx +18 -0
- cognits-0.0.1/frontend/src/components/Chat.tsx +254 -0
- cognits-0.0.1/frontend/src/components/CollapsibleSection.tsx +34 -0
- cognits-0.0.1/frontend/src/components/ContextMenu.tsx +70 -0
- cognits-0.0.1/frontend/src/components/DragOverlay.tsx +103 -0
- cognits-0.0.1/frontend/src/components/Dropdown.tsx +104 -0
- cognits-0.0.1/frontend/src/components/LearnitView.tsx +168 -0
- cognits-0.0.1/frontend/src/components/MarkdownView.tsx +11 -0
- cognits-0.0.1/frontend/src/components/ReportView.tsx +67 -0
- cognits-0.0.1/frontend/src/components/Sessions.tsx +141 -0
- cognits-0.0.1/frontend/src/components/Settings.tsx +738 -0
- cognits-0.0.1/frontend/src/components/TabBar.tsx +93 -0
- cognits-0.0.1/frontend/src/components/Viewport.tsx +215 -0
- cognits-0.0.1/frontend/src/components/Write.tsx +33 -0
- cognits-0.0.1/frontend/src/drag/drag-state.ts +111 -0
- cognits-0.0.1/frontend/src/index.css +214 -0
- cognits-0.0.1/frontend/src/index.tsx +6 -0
- cognits-0.0.1/frontend/src/lib/chat-stream.ts +165 -0
- cognits-0.0.1/frontend/src/lib/clipboard.ts +21 -0
- cognits-0.0.1/frontend/src/lib/markdown.ts +132 -0
- cognits-0.0.1/frontend/src/stores/chat-store.ts +339 -0
- cognits-0.0.1/frontend/src/stores/desktop-store.ts +147 -0
- cognits-0.0.1/frontend/src/stores/learnit-store.ts +73 -0
- cognits-0.0.1/frontend/src/stores/report-store.ts +42 -0
- cognits-0.0.1/frontend/src/stores/session-store.ts +69 -0
- cognits-0.0.1/frontend/src/stores/settings-store.ts +150 -0
- cognits-0.0.1/frontend/src/stores/viewport-tree-store.ts +362 -0
- cognits-0.0.1/frontend/src/tabs.ts +26 -0
- cognits-0.0.1/frontend/src/types.ts +50 -0
- cognits-0.0.1/pyproject.toml +32 -0
- cognits-0.0.1/src/cognits/__init__.py +6 -0
- cognits-0.0.1/src/cognits/__main__.py +3 -0
- cognits-0.0.1/src/cognits/agent/__init__.py +0 -0
- cognits-0.0.1/src/cognits/agent/agent.py +148 -0
- cognits-0.0.1/src/cognits/agent/prompts.py +42 -0
- cognits-0.0.1/src/cognits/agent/subagents.py +268 -0
- cognits-0.0.1/src/cognits/agent/tool_deploy.py +180 -0
- cognits-0.0.1/src/cognits/agent/tool_rag.py +58 -0
- cognits-0.0.1/src/cognits/llm/__init__.py +0 -0
- cognits-0.0.1/src/cognits/llm/deepseek.py +99 -0
- cognits-0.0.1/src/cognits/llm/types.py +52 -0
- cognits-0.0.1/src/cognits/main.py +127 -0
- cognits-0.0.1/src/cognits/paths.py +65 -0
- cognits-0.0.1/src/cognits/rag/__init__.py +0 -0
- cognits-0.0.1/src/cognits/rag/chunker.py +110 -0
- cognits-0.0.1/src/cognits/rag/engine.py +172 -0
- cognits-0.0.1/src/cognits/server/__init__.py +0 -0
- cognits-0.0.1/src/cognits/server/app.py +123 -0
- cognits-0.0.1/src/cognits/server/browser.py +56 -0
- cognits-0.0.1/src/cognits/server/devproxy.py +105 -0
- cognits-0.0.1/src/cognits/server/frontend.py +64 -0
- cognits-0.0.1/src/cognits/server/routes_chat.py +334 -0
- cognits-0.0.1/src/cognits/server/routes_config.py +71 -0
- cognits-0.0.1/src/cognits/server/routes_misc.py +163 -0
- cognits-0.0.1/src/cognits/server/routes_reports.py +63 -0
- cognits-0.0.1/src/cognits/server/routes_sessions.py +94 -0
- cognits-0.0.1/src/cognits/server/routes_stream.py +133 -0
- cognits-0.0.1/src/cognits/server/session_agent.py +85 -0
- cognits-0.0.1/src/cognits/server/util.py +28 -0
- cognits-0.0.1/src/cognits/storage/__init__.py +0 -0
- cognits-0.0.1/src/cognits/storage/db.py +467 -0
- cognits-0.0.1/src/cognits/storage/files.py +280 -0
- cognits-0.0.1/src/cognits/tinyfish.py +53 -0
- cognits-0.0.1/src/cognits/tools.py +59 -0
- cognits-0.0.1/tests/test_agent.py +128 -0
- cognits-0.0.1/tests/test_chunker.py +57 -0
- cognits-0.0.1/tests/test_crypto.py +51 -0
- cognits-0.0.1/tests/test_db.py +145 -0
- cognits-0.0.1/tests/test_sse.py +163 -0
cognits-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
.venv/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
dist/
|
|
7
|
+
|
|
8
|
+
# Runtime data (sessions, DB, RAG index, encrypted keys)
|
|
9
|
+
.learnit/
|
|
10
|
+
.cognits/
|
|
11
|
+
|
|
12
|
+
# Frontend build artifacts
|
|
13
|
+
frontend/node_modules/
|
|
14
|
+
frontend/dist/
|
|
15
|
+
src/cognits/frontend_dist/
|
|
16
|
+
|
|
17
|
+
# Internal design docs and dev tooling
|
|
18
|
+
IDEA.md
|
|
19
|
+
CONVERSACION_CON_LA_IDEA.md
|
|
20
|
+
AGENTS.md
|
|
21
|
+
.opencode/
|
|
22
|
+
opencode.jsonc
|
|
23
|
+
.claude/
|
|
24
|
+
|
|
25
|
+
# Legacy Go code (pre-Python rewrite, kept for reference)
|
|
26
|
+
legacy/
|
|
27
|
+
|
|
28
|
+
# Local
|
|
29
|
+
nohup.out
|
|
30
|
+
*.log
|
cognits-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eduardo Suárez
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
cognits-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cognits
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Cognits — Context-Oriented Generation for Neural Intelligent Tutoring Systems. Multi-agent AI personal tutor.
|
|
5
|
+
Author-email: Eduardo Suárez <edusrez@proton.me>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: <3.14,>=3.11
|
|
9
|
+
Requires-Dist: chromadb==1.5.9
|
|
10
|
+
Requires-Dist: cryptography>=42
|
|
11
|
+
Requires-Dist: fastapi>=0.115
|
|
12
|
+
Requires-Dist: fastembed==0.8.0
|
|
13
|
+
Requires-Dist: httpx>=0.27
|
|
14
|
+
Requires-Dist: uvicorn[standard]>=0.30
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Cognits
|
|
18
|
+
|
|
19
|
+
**C**ontext-**O**riented **G**eneration for **N**eural **I**ntelligent **T**utoring **S**ystems.
|
|
20
|
+
|
|
21
|
+
Multi-agent AI personal tutor: a Socratic orchestrator coordinates subagents
|
|
22
|
+
(documentalist with local RAG, web researcher) to guide your learning from a
|
|
23
|
+
local web interface, anchored to the project folder you're learning about.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv tool install cognits
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> The installation includes the local RAG engine (onnxruntime + ChromaDB, ~600 MB).
|
|
32
|
+
> On first launch, the BGE-M3 embeddings model is downloaded (~2.3 GB).
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd my-learning-project
|
|
38
|
+
cognits
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Starts a local server (port 5173 by default, `PORT` env var) and opens the
|
|
42
|
+
interface in your browser. State lives in `./.cognits/` (sessions, reports,
|
|
43
|
+
encrypted configuration, RAG index).
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
scripts/dev.sh # Vite (HMR) + uvicorn --reload
|
|
49
|
+
scripts/build.sh # frontend build + wheel
|
|
50
|
+
uv run pytest
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The frontend is a SolidJS SPA in `frontend/`; the backend is Python (FastAPI)
|
|
54
|
+
in `src/cognits/`.
|
cognits-0.0.1/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Cognits
|
|
2
|
+
|
|
3
|
+
**C**ontext-**O**riented **G**eneration for **N**eural **I**ntelligent **T**utoring **S**ystems.
|
|
4
|
+
|
|
5
|
+
Multi-agent AI personal tutor: a Socratic orchestrator coordinates subagents
|
|
6
|
+
(documentalist with local RAG, web researcher) to guide your learning from a
|
|
7
|
+
local web interface, anchored to the project folder you're learning about.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv tool install cognits
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
> The installation includes the local RAG engine (onnxruntime + ChromaDB, ~600 MB).
|
|
16
|
+
> On first launch, the BGE-M3 embeddings model is downloaded (~2.3 GB).
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
cd my-learning-project
|
|
22
|
+
cognits
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Starts a local server (port 5173 by default, `PORT` env var) and opens the
|
|
26
|
+
interface in your browser. State lives in `./.cognits/` (sessions, reports,
|
|
27
|
+
encrypted configuration, RAG index).
|
|
28
|
+
|
|
29
|
+
## Development
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
scripts/dev.sh # Vite (HMR) + uvicorn --reload
|
|
33
|
+
scripts/build.sh # frontend build + wheel
|
|
34
|
+
uv run pytest
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The frontend is a SolidJS SPA in `frontend/`; the backend is Python (FastAPI)
|
|
38
|
+
in `src/cognits/`.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createEffect, createMemo, Show, createSignal, onCleanup, onMount } from "solid-js"
|
|
2
|
+
import {
|
|
3
|
+
rootId,
|
|
4
|
+
getViewportData,
|
|
5
|
+
getSplitData,
|
|
6
|
+
setFraction,
|
|
7
|
+
moveTab,
|
|
8
|
+
placeSessionTabs,
|
|
9
|
+
removeSessionTabs,
|
|
10
|
+
type ViewportId,
|
|
11
|
+
} from "./stores/viewport-tree-store"
|
|
12
|
+
import { dragState, endDrag } from "./drag/drag-state"
|
|
13
|
+
import { activeSessionId } from "./stores/session-store"
|
|
14
|
+
import { loadConfig, defaultChatViewport, defaultWriteViewport, loadSessionConfig } from "./stores/settings-store"
|
|
15
|
+
import { loadSessionMessages } from "./stores/chat-store"
|
|
16
|
+
import { initDesktops, createDesktop, switchDesktop, closeDesktop, desktopCount, activeDesktopIndex } from "./stores/desktop-store"
|
|
17
|
+
import Viewport from "./components/Viewport"
|
|
18
|
+
import DragOverlay from "./components/DragOverlay"
|
|
19
|
+
|
|
20
|
+
initDesktops()
|
|
21
|
+
|
|
22
|
+
export default function App() {
|
|
23
|
+
createEffect(() => {
|
|
24
|
+
const sid = activeSessionId()
|
|
25
|
+
if (sid) {
|
|
26
|
+
placeSessionTabs(defaultChatViewport(), defaultWriteViewport())
|
|
27
|
+
loadSessionMessages(sid)
|
|
28
|
+
loadSessionConfig(sid)
|
|
29
|
+
} else {
|
|
30
|
+
removeSessionTabs()
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const handleDrop = () => {
|
|
35
|
+
const ds = dragState()
|
|
36
|
+
if (ds.targetViewport !== null) {
|
|
37
|
+
moveTab(ds.tabId, ds.sourceViewport, ds.targetViewport, ds.insertIndex)
|
|
38
|
+
}
|
|
39
|
+
endDrag()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onCleanup(() => {
|
|
43
|
+
endDrag()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
onMount(() => {
|
|
47
|
+
loadConfig()
|
|
48
|
+
const handler = (e: KeyboardEvent) => {
|
|
49
|
+
const tag = (e.target as HTMLElement).tagName
|
|
50
|
+
if (tag === "INPUT" || tag === "TEXTAREA") return
|
|
51
|
+
|
|
52
|
+
if (!e.ctrlKey && !e.shiftKey && !e.altKey) {
|
|
53
|
+
if (e.key === "n") {
|
|
54
|
+
e.preventDefault()
|
|
55
|
+
createDesktop()
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
const num = parseInt(e.key)
|
|
59
|
+
if (num >= 1 && num <= 9 && num <= desktopCount()) {
|
|
60
|
+
e.preventDefault()
|
|
61
|
+
switchDesktop(num - 1)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!e.ctrlKey && e.shiftKey && !e.altKey) {
|
|
65
|
+
if (e.key === "N") {
|
|
66
|
+
e.preventDefault()
|
|
67
|
+
closeDesktop(activeDesktopIndex())
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
document.addEventListener("keydown", handler)
|
|
72
|
+
onCleanup(() => document.removeEventListener("keydown", handler))
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
class="h-screen w-screen bg-black text-[#e0e0e0] select-none"
|
|
78
|
+
onContextMenu={(e) => e.preventDefault()}
|
|
79
|
+
>
|
|
80
|
+
<GridNode id={rootId()} />
|
|
81
|
+
<DragOverlay onDrop={handleDrop} />
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function GridNode(props: { id: ViewportId }) {
|
|
87
|
+
const split = createMemo(() => getSplitData(props.id))
|
|
88
|
+
const leaf = createMemo(() => getViewportData(props.id))
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Show when={split()} fallback={
|
|
92
|
+
<Show when={leaf()}>
|
|
93
|
+
{(vp) => (
|
|
94
|
+
<div style={{ width: "100%", height: "100%" }}>
|
|
95
|
+
<Viewport id={props.id} data={vp()} />
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
</Show>
|
|
99
|
+
}>
|
|
100
|
+
<SplitView id={props.id} />
|
|
101
|
+
</Show>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function SplitView(props: { id: ViewportId }) {
|
|
106
|
+
const split = createMemo(() => getSplitData(props.id)!)
|
|
107
|
+
const children = createMemo(() => split().children)
|
|
108
|
+
const f0 = createMemo(() => split().fractions[0])
|
|
109
|
+
const f1 = createMemo(() => split().fractions[1])
|
|
110
|
+
const isH = createMemo(() => split().direction === "h")
|
|
111
|
+
const [dragging, setDragging] = createSignal(false)
|
|
112
|
+
|
|
113
|
+
const onResizerDown = (e: MouseEvent) => {
|
|
114
|
+
e.preventDefault()
|
|
115
|
+
// Capture the container here: during the drag the pointer can leave
|
|
116
|
+
// this split and ev.target would point to another container.
|
|
117
|
+
const gridRef = (e.currentTarget as HTMLElement).closest("[data-split]") as HTMLElement | null
|
|
118
|
+
if (!gridRef) return
|
|
119
|
+
setDragging(true)
|
|
120
|
+
|
|
121
|
+
const start = isH() ? e.clientX : e.clientY
|
|
122
|
+
const startF0 = f0()
|
|
123
|
+
const startF1 = f1()
|
|
124
|
+
const totalFr = startF0 + startF1
|
|
125
|
+
|
|
126
|
+
const onMove = (ev: MouseEvent) => {
|
|
127
|
+
const current = isH() ? ev.clientX : ev.clientY
|
|
128
|
+
const delta = current - start
|
|
129
|
+
|
|
130
|
+
const rect = gridRef.getBoundingClientRect()
|
|
131
|
+
const size = isH() ? rect.width - 4 : rect.height - 4
|
|
132
|
+
|
|
133
|
+
if (size <= 0) return
|
|
134
|
+
const p0 = (startF0 * size) / totalFr
|
|
135
|
+
const p1 = Math.max(10, Math.min(size - 10, p0 + delta))
|
|
136
|
+
const clamped = (p1 * totalFr) / size
|
|
137
|
+
setFraction(props.id, 0, Math.max(0.5, clamped))
|
|
138
|
+
setFraction(props.id, 1, Math.max(0.5, totalFr - clamped))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const onUp = () => {
|
|
142
|
+
setDragging(false)
|
|
143
|
+
document.removeEventListener("mousemove", onMove)
|
|
144
|
+
document.removeEventListener("mouseup", onUp)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
document.addEventListener("mousemove", onMove)
|
|
148
|
+
document.addEventListener("mouseup", onUp)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
data-split
|
|
154
|
+
style={{
|
|
155
|
+
display: "grid",
|
|
156
|
+
overflow: "hidden",
|
|
157
|
+
width: "100%",
|
|
158
|
+
height: "100%",
|
|
159
|
+
...(isH()
|
|
160
|
+
? {
|
|
161
|
+
"grid-template-columns": `minmax(0, ${f0()}fr) 4px minmax(0, ${f1()}fr)`,
|
|
162
|
+
"grid-template-rows": "minmax(0, 1fr)",
|
|
163
|
+
}
|
|
164
|
+
: {
|
|
165
|
+
"grid-template-columns": "minmax(0, 1fr)",
|
|
166
|
+
"grid-template-rows": `minmax(0, ${f0()}fr) 4px minmax(0, ${f1()}fr)`,
|
|
167
|
+
}),
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<GridNode id={children()[0]} />
|
|
171
|
+
<div
|
|
172
|
+
class="resizer"
|
|
173
|
+
classList={{ "resizer-dragging": dragging() }}
|
|
174
|
+
style={{ cursor: isH() ? "col-resize" : "row-resize" }}
|
|
175
|
+
onMouseDown={onResizerDown}
|
|
176
|
+
/>
|
|
177
|
+
<GridNode id={children()[1]} />
|
|
178
|
+
</div>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createSignal, For, Show } from "solid-js"
|
|
2
|
+
import type { FileNode } from "./types"
|
|
3
|
+
|
|
4
|
+
function ChevronRight() {
|
|
5
|
+
return (
|
|
6
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
7
|
+
<path d="M9 18l6-6-6-6" />
|
|
8
|
+
</svg>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ChevronDown() {
|
|
13
|
+
return (
|
|
14
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
15
|
+
<path d="M6 9l6 6 6-6" />
|
|
16
|
+
</svg>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function FolderIcon() {
|
|
21
|
+
return (
|
|
22
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
23
|
+
<path d="M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z" />
|
|
24
|
+
</svg>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function FileIcon() {
|
|
29
|
+
return (
|
|
30
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
31
|
+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
|
32
|
+
<polyline points="14 2 14 8 20 8" />
|
|
33
|
+
</svg>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default function FileTree(props: { node: () => FileNode | null }) {
|
|
38
|
+
return (
|
|
39
|
+
<div style="padding: 4px 0">
|
|
40
|
+
<Show when={props.node()}>
|
|
41
|
+
{(n) => <NodeView node={n()} depth={0} />}
|
|
42
|
+
</Show>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ToggleIcon(props: { expanded: boolean }) {
|
|
48
|
+
return (
|
|
49
|
+
<span style={{ display: "inline-flex", "align-items": "center", width: "14px", color: "#6a6a6a" }}>
|
|
50
|
+
{props.expanded ? <ChevronDown /> : <ChevronRight />}
|
|
51
|
+
</span>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function NodeView(props: { node: FileNode; depth: number }) {
|
|
56
|
+
const isRoot = props.depth === 0
|
|
57
|
+
const [expanded, setExpanded] = createSignal(isRoot)
|
|
58
|
+
const hasKids = props.node.isDir && props.node.children && props.node.children.length > 0
|
|
59
|
+
|
|
60
|
+
function handleClick() {
|
|
61
|
+
if (!isRoot && hasKids) setExpanded(!expanded())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<div
|
|
67
|
+
style={{
|
|
68
|
+
cursor: isRoot ? "default" : "pointer",
|
|
69
|
+
padding: `1px 8px 1px ${8 + props.depth * 14}px`,
|
|
70
|
+
"font-size": "13px",
|
|
71
|
+
"line-height": "1.5",
|
|
72
|
+
"white-space": "nowrap",
|
|
73
|
+
overflow: "hidden",
|
|
74
|
+
"text-overflow": "ellipsis",
|
|
75
|
+
display: "flex",
|
|
76
|
+
"align-items": "center",
|
|
77
|
+
gap: "4px",
|
|
78
|
+
}}
|
|
79
|
+
onClick={handleClick}
|
|
80
|
+
>
|
|
81
|
+
<Show when={!isRoot && hasKids} fallback={
|
|
82
|
+
<span style={{ display: "inline-flex", "align-items": "center", width: "14px" }}>
|
|
83
|
+
{props.node.isDir ? (
|
|
84
|
+
<span style={{ color: "#6a6a6a" }}><FolderIcon /></span>
|
|
85
|
+
) : (
|
|
86
|
+
<span style={{ color: "#6a6a6a" }}><FileIcon /></span>
|
|
87
|
+
)}
|
|
88
|
+
</span>
|
|
89
|
+
}>
|
|
90
|
+
<ToggleIcon expanded={expanded()} />
|
|
91
|
+
</Show>
|
|
92
|
+
<span style={{ color: "#e0e0e0" }}>{props.node.name}</span>
|
|
93
|
+
</div>
|
|
94
|
+
<Show when={hasKids && (isRoot || expanded())}>
|
|
95
|
+
<For each={props.node.children}>
|
|
96
|
+
{(child) => <NodeView node={child} depth={props.depth + 1} />}
|
|
97
|
+
</For>
|
|
98
|
+
</Show>
|
|
99
|
+
</>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createSignal, onMount } from "solid-js"
|
|
2
|
+
import FileTree from "./FileTree"
|
|
3
|
+
import type { FileNode } from "./types"
|
|
4
|
+
|
|
5
|
+
export default function FileTreeWrapper() {
|
|
6
|
+
const [tree, setTree] = createSignal<FileNode | null>(null)
|
|
7
|
+
|
|
8
|
+
onMount(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const res = await fetch("/api/tree")
|
|
11
|
+
setTree(await res.json())
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.error(err)
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return <FileTree node={tree} />
|
|
18
|
+
}
|