bahadur3093 commited on
Commit
e14d9d9
·
1 Parent(s): 227e8b7

Add model management (pull + delete)

Browse files
Files changed (4) hide show
  1. pages/api/models.ts +44 -0
  2. pages/api/pull.ts +33 -0
  3. pages/index.tsx +178 -40
  4. styles/globals.css +102 -5
pages/api/models.ts ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextApiRequest, NextApiResponse } from "next"
2
+
3
+ const OLLAMA = "http://localhost:11434"
4
+
5
+ export default async function handler(
6
+ req: NextApiRequest,
7
+ res: NextApiResponse
8
+ ) {
9
+ // GET → list all models
10
+ if (req.method === "GET") {
11
+ try {
12
+ const r = await fetch(`${OLLAMA}/api/tags`)
13
+ const data = await r.json()
14
+ res.status(200).json(data)
15
+ } catch {
16
+ res.status(500).json({ error: "Failed to fetch models" })
17
+ }
18
+ return
19
+ }
20
+
21
+ // DELETE → remove a model
22
+ if (req.method === "DELETE") {
23
+ const { name } = req.body
24
+ try {
25
+ const r = await fetch(`${OLLAMA}/api/delete`, {
26
+ method: "DELETE",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ name }),
29
+ })
30
+
31
+ if (r.ok) {
32
+ res.status(200).json({ success: true })
33
+ } else {
34
+ const err = await r.text()
35
+ res.status(400).json({ error: err })
36
+ }
37
+ } catch {
38
+ res.status(500).json({ error: "Failed to delete model" })
39
+ }
40
+ return
41
+ }
42
+
43
+ res.status(405).json({ error: "Method not allowed" })
44
+ }
pages/api/pull.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextApiRequest, NextApiResponse } from "next"
2
+
3
+ export default async function handler(
4
+ req: NextApiRequest,
5
+ res: NextApiResponse
6
+ ) {
7
+ if (req.method !== "POST") {
8
+ return res.status(405).json({ error: "Method not allowed" })
9
+ }
10
+
11
+ const { name } = req.body
12
+
13
+ if (!name || !name.trim()) {
14
+ return res.status(400).json({ error: "Model name required" })
15
+ }
16
+
17
+ try {
18
+ const r = await fetch("http://localhost:11434/api/pull", {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ body: JSON.stringify({ name, stream: false }),
22
+ })
23
+
24
+ if (r.ok) {
25
+ res.status(200).json({ success: true, model: name })
26
+ } else {
27
+ const err = await r.text()
28
+ res.status(400).json({ error: err })
29
+ }
30
+ } catch {
31
+ res.status(500).json({ error: "Failed to pull model" })
32
+ }
33
+ }
pages/index.tsx CHANGED
@@ -1,20 +1,104 @@
1
- import { useState } from "react"
2
 
3
- const models = [
4
- "qwen2.5:1.5b",
5
- "mistral",
6
- "tinyllama",
7
- "hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF",
8
- ]
9
 
10
  export default function Home() {
11
- const [model, setModel] = useState(models[0])
 
 
 
 
 
 
 
 
12
  const [prompt, setPrompt] = useState("")
13
  const [response, setResponse] = useState("")
14
  const [loading, setLoading] = useState(false)
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  const generate = async () => {
17
- if (!prompt.trim()) return
18
  setLoading(true)
19
  setResponse("")
20
 
@@ -38,38 +122,92 @@ export default function Home() {
38
  <h1>⚡ Ollama UI</h1>
39
  <p className="subtitle">Self-hosted LLM on Hugging Face</p>
40
 
41
- <div className="field">
42
- <label>Model</label>
43
- <select value={model} onChange={(e) => setModel(e.target.value)}>
44
- {models.map((m) => (
45
- <option key={m} value={m}>{m}</option>
46
- ))}
47
- </select>
48
- </div>
49
-
50
- <div className="field">
51
- <label>Prompt</label>
52
- <textarea
53
- rows={5}
54
- placeholder="Type your prompt here..."
55
- value={prompt}
56
- onChange={(e) => setPrompt(e.target.value)}
57
- onKeyDown={(e) => {
58
- if (e.key === "Enter" && e.metaKey) generate()
59
- }}
60
- />
61
- </div>
62
-
63
- <button onClick={generate} disabled={loading}>
64
- {loading ? "⏳ Generating..." : "🚀 Generate"}
65
- </button>
66
-
67
- <div className="response">
68
- <label>Response</label>
69
- <div className="response-box">
70
- {response || "Response will appear here..."}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  </div>
72
- </div>
73
  </main>
74
  )
75
  }
 
1
+ import { useState, useEffect } from "react"
2
 
3
+ interface OllamaModel {
4
+ name: string
5
+ size: number
6
+ }
 
 
7
 
8
  export default function Home() {
9
+ // Model management
10
+ const [installedModels, setInstalledModels] = useState<OllamaModel[]>([])
11
+ const [newModel, setNewModel] = useState("")
12
+ const [pulling, setPulling] = useState(false)
13
+ const [pullStatus, setPullStatus] = useState("")
14
+ const [deleting, setDeleting] = useState("")
15
+
16
+ // Generation
17
+ const [model, setModel] = useState("")
18
  const [prompt, setPrompt] = useState("")
19
  const [response, setResponse] = useState("")
20
  const [loading, setLoading] = useState(false)
21
 
22
+ // Fetch installed models
23
+ const fetchModels = async () => {
24
+ try {
25
+ const res = await fetch("/api/models")
26
+ const data = await res.json()
27
+ const models = data.models || []
28
+ setInstalledModels(models)
29
+ if (models.length > 0 && !model) {
30
+ setModel(models[0].name)
31
+ }
32
+ } catch {
33
+ console.error("Failed to fetch models")
34
+ }
35
+ }
36
+
37
+ useEffect(() => {
38
+ fetchModels()
39
+ }, [])
40
+
41
+ // Pull new model
42
+ const pullModel = async () => {
43
+ if (!newModel.trim()) return
44
+ setPulling(true)
45
+ setPullStatus(`⬇️ Pulling ${newModel}... this may take a while`)
46
+
47
+ try {
48
+ const res = await fetch("/api/pull", {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify({ name: newModel.trim() }),
52
+ })
53
+
54
+ if (res.ok) {
55
+ setPullStatus(`✅ ${newModel} pulled successfully!`)
56
+ setNewModel("")
57
+ await fetchModels()
58
+ } else {
59
+ const data = await res.json()
60
+ setPullStatus(`❌ ${data.error || "Failed to pull"}`)
61
+ }
62
+ } catch {
63
+ setPullStatus("❌ Failed to connect")
64
+ }
65
+
66
+ setPulling(false)
67
+ setTimeout(() => setPullStatus(""), 5000)
68
+ }
69
+
70
+ // Delete model
71
+ const deleteModel = async (name: string) => {
72
+ if (!confirm(`Delete ${name}?`)) return
73
+ setDeleting(name)
74
+
75
+ try {
76
+ const res = await fetch("/api/models", {
77
+ method: "DELETE",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify({ name }),
80
+ })
81
+
82
+ if (res.ok) {
83
+ await fetchModels()
84
+ if (model === name) setModel("")
85
+ }
86
+ } catch {
87
+ console.error("Failed to delete")
88
+ }
89
+
90
+ setDeleting("")
91
+ }
92
+
93
+ // Format size
94
+ const formatSize = (bytes: number) => {
95
+ const gb = bytes / 1e9
96
+ return gb >= 1 ? `${gb.toFixed(1)} GB` : `${(bytes / 1e6).toFixed(0)} MB`
97
+ }
98
+
99
+ // Generate
100
  const generate = async () => {
101
+ if (!prompt.trim() || !model) return
102
  setLoading(true)
103
  setResponse("")
104
 
 
122
  <h1>⚡ Ollama UI</h1>
123
  <p className="subtitle">Self-hosted LLM on Hugging Face</p>
124
 
125
+ {/* ── Model Management ── */}
126
+ <section className="section">
127
+ <h2>📦 Model Management</h2>
128
+
129
+ {/* Pull new model */}
130
+ <div className="pull-row">
131
+ <input
132
+ type="text"
133
+ placeholder="e.g. mistral, tinyllama, hf.co/user/repo"
134
+ value={newModel}
135
+ onChange={(e) => setNewModel(e.target.value)}
136
+ onKeyDown={(e) => {
137
+ if (e.key === "Enter") pullModel()
138
+ }}
139
+ disabled={pulling}
140
+ />
141
+ <button onClick={pullModel} disabled={pulling} className="btn-pull">
142
+ {pulling ? "Pulling..." : "⬇️ Pull"}
143
+ </button>
144
+ </div>
145
+
146
+ {pullStatus && <p className="status">{pullStatus}</p>}
147
+
148
+ {/* Installed models list */}
149
+ <div className="model-list">
150
+ {installedModels.length === 0 ? (
151
+ <p className="empty">No models installed yet</p>
152
+ ) : (
153
+ installedModels.map((m) => (
154
+ <div key={m.name} className="model-item">
155
+ <div className="model-info">
156
+ <span className="model-name">{m.name}</span>
157
+ <span className="model-size">{formatSize(m.size)}</span>
158
+ </div>
159
+ <button
160
+ onClick={() => deleteModel(m.name)}
161
+ disabled={deleting === m.name}
162
+ className="btn-delete"
163
+ >
164
+ {deleting === m.name ? "..." : "🗑️"}
165
+ </button>
166
+ </div>
167
+ ))
168
+ )}
169
+ </div>
170
+ </section>
171
+
172
+ {/* ── Generation ── */}
173
+ <section className="section">
174
+ <h2>💬 Generate</h2>
175
+
176
+ <div className="field">
177
+ <label>Model</label>
178
+ <select value={model} onChange={(e) => setModel(e.target.value)}>
179
+ {installedModels.map((m) => (
180
+ <option key={m.name} value={m.name}>
181
+ {m.name}
182
+ </option>
183
+ ))}
184
+ </select>
185
+ </div>
186
+
187
+ <div className="field">
188
+ <label>Prompt</label>
189
+ <textarea
190
+ rows={5}
191
+ placeholder="Type your prompt here..."
192
+ value={prompt}
193
+ onChange={(e) => setPrompt(e.target.value)}
194
+ onKeyDown={(e) => {
195
+ if (e.key === "Enter" && e.metaKey) generate()
196
+ }}
197
+ />
198
+ </div>
199
+
200
+ <button onClick={generate} disabled={loading || !model}>
201
+ {loading ? "⏳ Generating..." : "🚀 Generate"}
202
+ </button>
203
+
204
+ <div className="response">
205
+ <label>Response</label>
206
+ <div className="response-box">
207
+ {response || "Response will appear here..."}
208
+ </div>
209
  </div>
210
+ </section>
211
  </main>
212
  )
213
  }
styles/globals.css CHANGED
@@ -21,12 +21,28 @@ h1 {
21
  margin-bottom: 4px;
22
  }
23
 
 
 
 
 
 
 
24
  .subtitle {
25
  color: #888;
26
  font-size: 14px;
27
  margin-bottom: 32px;
28
  }
29
 
 
 
 
 
 
 
 
 
 
 
30
  .field {
31
  margin-bottom: 20px;
32
  }
@@ -41,9 +57,10 @@ label {
41
  }
42
 
43
  select,
44
- textarea {
 
45
  width: 100%;
46
- background: #141414;
47
  color: #e5e5e5;
48
  border: 1px solid #2a2a2a;
49
  border-radius: 8px;
@@ -54,7 +71,8 @@ textarea {
54
  }
55
 
56
  select:focus,
57
- textarea:focus {
 
58
  border-color: #555;
59
  }
60
 
@@ -63,6 +81,7 @@ textarea {
63
  font-family: inherit;
64
  }
65
 
 
66
  button {
67
  background: #fff;
68
  color: #0a0a0a;
@@ -84,12 +103,90 @@ button:disabled {
84
  cursor: not-allowed;
85
  }
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  .response {
88
- margin-top: 28px;
89
  }
90
 
91
  .response-box {
92
- background: #111;
93
  border: 1px solid #2a2a2a;
94
  border-radius: 8px;
95
  padding: 16px;
 
21
  margin-bottom: 4px;
22
  }
23
 
24
+ h2 {
25
+ font-size: 18px;
26
+ color: #ccc;
27
+ margin-bottom: 16px;
28
+ }
29
+
30
  .subtitle {
31
  color: #888;
32
  font-size: 14px;
33
  margin-bottom: 32px;
34
  }
35
 
36
+ /* Sections */
37
+ .section {
38
+ background: #111;
39
+ border: 1px solid #1e1e1e;
40
+ border-radius: 12px;
41
+ padding: 24px;
42
+ margin-bottom: 24px;
43
+ }
44
+
45
+ /* Fields */
46
  .field {
47
  margin-bottom: 20px;
48
  }
 
57
  }
58
 
59
  select,
60
+ textarea,
61
+ input[type="text"] {
62
  width: 100%;
63
+ background: #1a1a1a;
64
  color: #e5e5e5;
65
  border: 1px solid #2a2a2a;
66
  border-radius: 8px;
 
71
  }
72
 
73
  select:focus,
74
+ textarea:focus,
75
+ input[type="text"]:focus {
76
  border-color: #555;
77
  }
78
 
 
81
  font-family: inherit;
82
  }
83
 
84
+ /* Buttons */
85
  button {
86
  background: #fff;
87
  color: #0a0a0a;
 
103
  cursor: not-allowed;
104
  }
105
 
106
+ /* Pull row */
107
+ .pull-row {
108
+ display: flex;
109
+ gap: 10px;
110
+ margin-bottom: 12px;
111
+ }
112
+
113
+ .pull-row input {
114
+ flex: 1;
115
+ }
116
+
117
+ .btn-pull {
118
+ white-space: nowrap;
119
+ padding: 12px 20px;
120
+ font-size: 14px;
121
+ }
122
+
123
+ .status {
124
+ font-size: 13px;
125
+ color: #aaa;
126
+ margin-bottom: 16px;
127
+ }
128
+
129
+ /* Model list */
130
+ .model-list {
131
+ margin-top: 16px;
132
+ }
133
+
134
+ .model-item {
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: space-between;
138
+ padding: 12px 14px;
139
+ background: #1a1a1a;
140
+ border: 1px solid #222;
141
+ border-radius: 8px;
142
+ margin-bottom: 8px;
143
+ }
144
+
145
+ .model-info {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 2px;
149
+ }
150
+
151
+ .model-name {
152
+ font-size: 15px;
153
+ font-weight: 500;
154
+ color: #e5e5e5;
155
+ }
156
+
157
+ .model-size {
158
+ font-size: 12px;
159
+ color: #666;
160
+ }
161
+
162
+ .btn-delete {
163
+ background: transparent;
164
+ color: #888;
165
+ padding: 6px 10px;
166
+ font-size: 16px;
167
+ border-radius: 6px;
168
+ }
169
+
170
+ .btn-delete:hover {
171
+ background: #2a1515;
172
+ color: #ff6b6b;
173
+ opacity: 1;
174
+ }
175
+
176
+ .empty {
177
+ color: #555;
178
+ font-size: 14px;
179
+ text-align: center;
180
+ padding: 20px;
181
+ }
182
+
183
+ /* Response */
184
  .response {
185
+ margin-top: 20px;
186
  }
187
 
188
  .response-box {
189
+ background: #141414;
190
  border: 1px solid #2a2a2a;
191
  border-radius: 8px;
192
  padding: 16px;