Spaces:
Build error
Build error
Commit ·
e14d9d9
1
Parent(s): 227e8b7
Add model management (pull + delete)
Browse files- pages/api/models.ts +44 -0
- pages/api/pull.ts +33 -0
- pages/index.tsx +178 -40
- 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
"hf.co/bartowski/Llama-3.2-1B-Instruct-GGUF",
|
| 8 |
-
]
|
| 9 |
|
| 10 |
export default function Home() {
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 42 |
-
|
| 43 |
-
<
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
/>
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
{
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
</div>
|
| 72 |
-
</
|
| 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: #
|
| 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:
|
| 89 |
}
|
| 90 |
|
| 91 |
.response-box {
|
| 92 |
-
background: #
|
| 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;
|