Spaces:
Build error
Build error
Commit ·
227e8b7
1
Parent(s): 6bc4b86
next js ui
Browse files- Dockerfile +14 -4
- entrypoint.sh +8 -12
- next.config.js +6 -0
- package.json +19 -0
- pages/_app.tsx +6 -0
- pages/api/generate.ts +29 -0
- pages/index.tsx +75 -0
- styles/globals.css +101 -0
- tsconfig.json +20 -0
Dockerfile
CHANGED
|
@@ -1,14 +1,24 @@
|
|
| 1 |
FROM ubuntu:22.04
|
| 2 |
|
| 3 |
-
RUN apt-get update && apt-get install -y curl zstd
|
| 4 |
-
curl -fsSL https://
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
ENV OLLAMA_HOST=0.0.0.0:
|
| 7 |
ENV OLLAMA_ORIGINS=*
|
| 8 |
ENV HOME=/tmp
|
| 9 |
ENV OLLAMA_MODELS=/tmp/.ollama/models
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
COPY entrypoint.sh /entrypoint.sh
|
| 13 |
RUN chmod +x /entrypoint.sh
|
| 14 |
|
|
|
|
| 1 |
FROM ubuntu:22.04
|
| 2 |
|
| 3 |
+
RUN apt-get update && apt-get install -y curl zstd ca-certificates gnupg \
|
| 4 |
+
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
| 5 |
+
&& apt-get install -y nodejs \
|
| 6 |
+
&& curl -fsSL https://ollama.com/install.sh | sh \
|
| 7 |
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
| 8 |
|
| 9 |
+
ENV OLLAMA_HOST=0.0.0.0:11434
|
| 10 |
ENV OLLAMA_ORIGINS=*
|
| 11 |
ENV HOME=/tmp
|
| 12 |
ENV OLLAMA_MODELS=/tmp/.ollama/models
|
| 13 |
|
| 14 |
+
WORKDIR /app
|
| 15 |
+
|
| 16 |
+
COPY package.json tsconfig.json next.config.js ./
|
| 17 |
+
RUN npm install
|
| 18 |
+
|
| 19 |
+
COPY . .
|
| 20 |
+
RUN npm run build
|
| 21 |
+
|
| 22 |
COPY entrypoint.sh /entrypoint.sh
|
| 23 |
RUN chmod +x /entrypoint.sh
|
| 24 |
|
entrypoint.sh
CHANGED
|
@@ -1,26 +1,22 @@
|
|
| 1 |
#!/bin/bash
|
| 2 |
|
| 3 |
-
echo "🚀 Starting Ollama server..."
|
| 4 |
ollama serve &
|
| 5 |
|
| 6 |
-
# Wait for Ollama to be ready
|
| 7 |
-
echo "⏳ Waiting for Ollama to start..."
|
| 8 |
sleep 8
|
| 9 |
|
| 10 |
-
# Optional: preload a default model (DO NOT force remove anything)
|
| 11 |
DEFAULT_MODEL="qwen2.5:1.5b"
|
| 12 |
|
| 13 |
-
echo "📦 Checking
|
| 14 |
if ! ollama list | grep -q "$DEFAULT_MODEL"; then
|
| 15 |
-
echo "⬇️ Pulling
|
| 16 |
ollama pull $DEFAULT_MODEL
|
| 17 |
else
|
| 18 |
-
echo "✅ Model
|
| 19 |
fi
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
| 26 |
-
wait
|
|
|
|
| 1 |
#!/bin/bash
|
| 2 |
|
| 3 |
+
echo "🚀 Starting Ollama server on :11434..."
|
| 4 |
ollama serve &
|
| 5 |
|
|
|
|
|
|
|
| 6 |
sleep 8
|
| 7 |
|
|
|
|
| 8 |
DEFAULT_MODEL="qwen2.5:1.5b"
|
| 9 |
|
| 10 |
+
echo "📦 Checking model..."
|
| 11 |
if ! ollama list | grep -q "$DEFAULT_MODEL"; then
|
| 12 |
+
echo "⬇️ Pulling: $DEFAULT_MODEL"
|
| 13 |
ollama pull $DEFAULT_MODEL
|
| 14 |
else
|
| 15 |
+
echo "✅ Model ready: $DEFAULT_MODEL"
|
| 16 |
fi
|
| 17 |
|
| 18 |
+
echo "🎨 Starting Next.js UI on :7860..."
|
| 19 |
+
cd /app
|
| 20 |
+
npm start
|
| 21 |
|
| 22 |
+
wait
|
|
|
next.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = {
|
| 3 |
+
reactStrictMode: true,
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
module.exports = nextConfig
|
package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "ollama-ui",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"scripts": {
|
| 5 |
+
"dev": "next dev -p 7860 -H 0.0.0.0",
|
| 6 |
+
"build": "next build",
|
| 7 |
+
"start": "next start -p 7860 -H 0.0.0.0"
|
| 8 |
+
},
|
| 9 |
+
"dependencies": {
|
| 10 |
+
"next": "14.2.3",
|
| 11 |
+
"react": "^18.3.1",
|
| 12 |
+
"react-dom": "^18.3.1"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"@types/node": "^20.12.0",
|
| 16 |
+
"@types/react": "^18.3.0",
|
| 17 |
+
"typescript": "^5.4.0"
|
| 18 |
+
}
|
| 19 |
+
}
|
pages/_app.tsx
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { AppProps } from "next/app"
|
| 2 |
+
import "../styles/globals.css"
|
| 3 |
+
|
| 4 |
+
export default function App({ Component, pageProps }: AppProps) {
|
| 5 |
+
return <Component {...pageProps} />
|
| 6 |
+
}
|
pages/api/generate.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 { model, prompt } = req.body
|
| 12 |
+
|
| 13 |
+
try {
|
| 14 |
+
const ollamaRes = await fetch("http://localhost:11434/api/generate", {
|
| 15 |
+
method: "POST",
|
| 16 |
+
headers: { "Content-Type": "application/json" },
|
| 17 |
+
body: JSON.stringify({
|
| 18 |
+
model,
|
| 19 |
+
prompt,
|
| 20 |
+
stream: false,
|
| 21 |
+
}),
|
| 22 |
+
})
|
| 23 |
+
|
| 24 |
+
const data = await ollamaRes.json()
|
| 25 |
+
res.status(200).json(data)
|
| 26 |
+
} catch (err) {
|
| 27 |
+
res.status(500).json({ error: "Failed to connect to Ollama" })
|
| 28 |
+
}
|
| 29 |
+
}
|
pages/index.tsx
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
| 21 |
+
try {
|
| 22 |
+
const res = await fetch("/api/generate", {
|
| 23 |
+
method: "POST",
|
| 24 |
+
headers: { "Content-Type": "application/json" },
|
| 25 |
+
body: JSON.stringify({ model, prompt }),
|
| 26 |
+
})
|
| 27 |
+
const data = await res.json()
|
| 28 |
+
setResponse(data.response || data.error || "No response")
|
| 29 |
+
} catch {
|
| 30 |
+
setResponse("❌ Failed to connect to server")
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
setLoading(false)
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
return (
|
| 37 |
+
<main className="container">
|
| 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 |
+
}
|
styles/globals.css
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
box-sizing: border-box;
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
body {
|
| 8 |
+
background: #0a0a0a;
|
| 9 |
+
color: #e5e5e5;
|
| 10 |
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.container {
|
| 14 |
+
max-width: 720px;
|
| 15 |
+
margin: 0 auto;
|
| 16 |
+
padding: 48px 24px;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
h1 {
|
| 20 |
+
font-size: 28px;
|
| 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 |
+
}
|
| 33 |
+
|
| 34 |
+
label {
|
| 35 |
+
display: block;
|
| 36 |
+
font-size: 13px;
|
| 37 |
+
color: #999;
|
| 38 |
+
margin-bottom: 6px;
|
| 39 |
+
text-transform: uppercase;
|
| 40 |
+
letter-spacing: 0.5px;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
select,
|
| 44 |
+
textarea {
|
| 45 |
+
width: 100%;
|
| 46 |
+
background: #141414;
|
| 47 |
+
color: #e5e5e5;
|
| 48 |
+
border: 1px solid #2a2a2a;
|
| 49 |
+
border-radius: 8px;
|
| 50 |
+
padding: 12px;
|
| 51 |
+
font-size: 15px;
|
| 52 |
+
outline: none;
|
| 53 |
+
transition: border 0.2s;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
select:focus,
|
| 57 |
+
textarea:focus {
|
| 58 |
+
border-color: #555;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
textarea {
|
| 62 |
+
resize: vertical;
|
| 63 |
+
font-family: inherit;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
button {
|
| 67 |
+
background: #fff;
|
| 68 |
+
color: #0a0a0a;
|
| 69 |
+
border: none;
|
| 70 |
+
border-radius: 8px;
|
| 71 |
+
padding: 12px 24px;
|
| 72 |
+
font-size: 15px;
|
| 73 |
+
font-weight: 600;
|
| 74 |
+
cursor: pointer;
|
| 75 |
+
transition: opacity 0.2s;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
button:hover {
|
| 79 |
+
opacity: 0.85;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
button:disabled {
|
| 83 |
+
opacity: 0.4;
|
| 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;
|
| 96 |
+
min-height: 120px;
|
| 97 |
+
font-size: 15px;
|
| 98 |
+
line-height: 1.6;
|
| 99 |
+
white-space: pre-wrap;
|
| 100 |
+
color: #ccc;
|
| 101 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "es5",
|
| 4 |
+
"lib": ["dom", "es2017"],
|
| 5 |
+
"allowJs": true,
|
| 6 |
+
"skipLibCheck": true,
|
| 7 |
+
"strict": false,
|
| 8 |
+
"forceConsistentCasingInFileNames": true,
|
| 9 |
+
"noEmit": true,
|
| 10 |
+
"esModuleInterop": true,
|
| 11 |
+
"module": "esnext",
|
| 12 |
+
"moduleResolution": "node",
|
| 13 |
+
"resolveJsonModule": true,
|
| 14 |
+
"isolatedModules": true,
|
| 15 |
+
"jsx": "preserve",
|
| 16 |
+
"incremental": true
|
| 17 |
+
},
|
| 18 |
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
| 19 |
+
"exclude": ["node_modules"]
|
| 20 |
+
}
|