Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77dc1a6d74 | ||
|
|
05e08d2310 | ||
|
|
31590284a7 | ||
|
|
f2863cc7f8 | ||
|
|
4dd296e155 | ||
|
|
304f419429 | ||
|
|
2666d3c206 | ||
|
|
787d965331 | ||
|
|
e6eee0732c | ||
|
|
4c2b4589ac | ||
|
|
5571ed5248 | ||
|
|
0944b01e7d | ||
|
|
5028de2901 | ||
|
|
e1f0a0dc74 | ||
|
|
b227261f21 | ||
|
|
c63f811909 | ||
|
|
7c71c10d4f | ||
|
|
c5f7eadd87 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,5 @@
|
||||
.vscode
|
||||
.env
|
||||
.venv
|
||||
*.spec
|
||||
dist
|
||||
ollama
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
FROM golang:1.20
|
||||
RUN apt-get update && apt-get install -y cmake
|
||||
WORKDIR /go/src/github.com/jmorganca/ollama
|
||||
COPY . .
|
||||
RUN cmake -S llama -B llama/build && cmake --build llama/build
|
||||
RUN CGO_ENABLED=1 go build -ldflags '-linkmode external -extldflags "-static"' .
|
||||
|
||||
FROM alpine
|
||||
|
||||
@@ -18,7 +18,6 @@ Run large language models with `llama.cpp`.
|
||||
|
||||
- [Download](https://ollama.ai/download) for macOS
|
||||
- Download for Windows (coming soon)
|
||||
- Docker: `docker run -p 11434:11434 ollama/ollama`
|
||||
|
||||
You can also build the [binary from source](#building).
|
||||
|
||||
@@ -105,5 +104,5 @@ curl -X POST http://localhost:11343/api/pull -d '{"model": "orca"}'
|
||||
Complete a prompt
|
||||
|
||||
```
|
||||
curl -X POST http://localhost:11434/api/generate -d '{"model": "orca", "prompt": "hello!", "stream": true}'
|
||||
curl -X POST http://localhost:11434/api/generate -d '{"model": "orca", "prompt": "hello!"}'
|
||||
```
|
||||
|
||||
43
api/types.go
43
api/types.go
@@ -1,6 +1,11 @@
|
||||
package api
|
||||
|
||||
import "runtime"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PullRequest struct {
|
||||
Model string `json:"model"`
|
||||
@@ -20,7 +25,41 @@ type GenerateRequest struct {
|
||||
}
|
||||
|
||||
type GenerateResponse struct {
|
||||
Response string `json:"response"`
|
||||
Model string `json:"model"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Response string `json:"response,omitempty"`
|
||||
|
||||
Done bool `json:"done"`
|
||||
|
||||
TotalDuration time.Duration `json:"total_duration,omitempty"`
|
||||
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
|
||||
PromptEvalDuration time.Duration `json:"prompt_eval_duration,omitempty"`
|
||||
EvalCount int `json:"eval_count,omitempty"`
|
||||
EvalDuration time.Duration `json:"eval_duration,omitempty"`
|
||||
}
|
||||
|
||||
func (r *GenerateResponse) Summary() {
|
||||
if r.TotalDuration > 0 {
|
||||
fmt.Fprintf(os.Stderr, "total duration: %v\n", r.TotalDuration)
|
||||
}
|
||||
|
||||
if r.PromptEvalCount > 0 {
|
||||
fmt.Fprintf(os.Stderr, "prompt eval count: %d token(s)\n", r.PromptEvalCount)
|
||||
}
|
||||
|
||||
if r.PromptEvalDuration > 0 {
|
||||
fmt.Fprintf(os.Stderr, "prompt eval duration: %s\n", r.PromptEvalDuration)
|
||||
fmt.Fprintf(os.Stderr, "prompt eval rate: %.2f tokens/s\n", float64(r.PromptEvalCount)/r.PromptEvalDuration.Seconds())
|
||||
}
|
||||
|
||||
if r.EvalCount > 0 {
|
||||
fmt.Fprintf(os.Stderr, "eval count: %d token(s)\n", r.EvalCount)
|
||||
}
|
||||
|
||||
if r.EvalDuration > 0 {
|
||||
fmt.Fprintf(os.Stderr, "eval duraiton: %s\n", r.EvalDuration)
|
||||
fmt.Fprintf(os.Stderr, "eval rate: %.2f tokens/s\n", float64(r.EvalCount)/r.EvalDuration.Seconds())
|
||||
}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
|
||||
38
cmd/cmd.go
38
cmd/cmd.go
@@ -59,7 +59,7 @@ func pull(model string) error {
|
||||
&api.PullRequest{Model: model},
|
||||
func(progress api.PullProgress) error {
|
||||
if bar == nil {
|
||||
if progress.Percent == 100 {
|
||||
if progress.Percent >= 100 {
|
||||
// already downloaded
|
||||
return nil
|
||||
}
|
||||
@@ -72,21 +72,20 @@ func pull(model string) error {
|
||||
)
|
||||
}
|
||||
|
||||
func RunGenerate(_ *cobra.Command, args []string) error {
|
||||
// join all args into a single prompt
|
||||
prompt := strings.Join(args[1:], " ")
|
||||
func RunGenerate(cmd *cobra.Command, args []string) error {
|
||||
if len(args) > 1 {
|
||||
return generate(args[0], prompt)
|
||||
// join all args into a single prompt
|
||||
return generate(cmd, args[0], strings.Join(args[1:], " "))
|
||||
}
|
||||
|
||||
if term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return generateInteractive(args[0])
|
||||
return generateInteractive(cmd, args[0])
|
||||
}
|
||||
|
||||
return generateBatch(args[0])
|
||||
return generateBatch(cmd, args[0])
|
||||
}
|
||||
|
||||
func generate(model, prompt string) error {
|
||||
func generate(cmd *cobra.Command, model, prompt string) error {
|
||||
if len(strings.TrimSpace(prompt)) > 0 {
|
||||
client := api.NewClient()
|
||||
|
||||
@@ -109,12 +108,16 @@ func generate(model, prompt string) error {
|
||||
}
|
||||
}()
|
||||
|
||||
var latest api.GenerateResponse
|
||||
|
||||
request := api.GenerateRequest{Model: model, Prompt: prompt}
|
||||
fn := func(resp api.GenerateResponse) error {
|
||||
if !spinner.IsFinished() {
|
||||
spinner.Finish()
|
||||
}
|
||||
|
||||
latest = resp
|
||||
|
||||
fmt.Print(resp.Response)
|
||||
return nil
|
||||
}
|
||||
@@ -125,16 +128,25 @@ func generate(model, prompt string) error {
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println()
|
||||
|
||||
verbose, err := cmd.Flags().GetBool("verbose")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if verbose {
|
||||
latest.Summary()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateInteractive(model string) error {
|
||||
func generateInteractive(cmd *cobra.Command, model string) error {
|
||||
fmt.Print(">>> ")
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
if err := generate(model, scanner.Text()); err != nil {
|
||||
if err := generate(cmd, model, scanner.Text()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -144,12 +156,12 @@ func generateInteractive(model string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateBatch(model string) error {
|
||||
func generateBatch(cmd *cobra.Command, model string) error {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
prompt := scanner.Text()
|
||||
fmt.Printf(">>> %s\n", prompt)
|
||||
if err := generate(model, prompt); err != nil {
|
||||
if err := generate(cmd, model, prompt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -201,6 +213,8 @@ func NewCLI() *cobra.Command {
|
||||
RunE: RunRun,
|
||||
}
|
||||
|
||||
runCmd.Flags().Bool("verbose", false, "Show timings for response")
|
||||
|
||||
serveCmd := &cobra.Command{
|
||||
Use: "serve",
|
||||
Aliases: []string{"start"},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build darwin
|
||||
|
||||
/**
|
||||
* llama.cpp - git 5bf2a2771886ee86137e01dbc7492f78fb392066
|
||||
*
|
||||
|
||||
@@ -79,9 +79,11 @@ llama_token llama_sample(
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/jmorganca/ollama/api"
|
||||
@@ -123,7 +125,14 @@ func New(model string, opts api.Options) (*llama, error) {
|
||||
defer C.free(unsafe.Pointer(cModel))
|
||||
|
||||
llm.model = C.llama_load_model_from_file(cModel, params)
|
||||
if llm.model == nil {
|
||||
return nil, errors.New("failed to load model")
|
||||
}
|
||||
|
||||
llm.ctx = C.llama_new_context_with_model(llm.model, params)
|
||||
if llm.ctx == nil {
|
||||
return nil, errors.New("failed to create context")
|
||||
}
|
||||
|
||||
// warm up the model
|
||||
bos := []C.llama_token{C.llama_token_bos()}
|
||||
@@ -140,7 +149,7 @@ func (llm *llama) Close() {
|
||||
C.llama_print_timings(llm.ctx)
|
||||
}
|
||||
|
||||
func (llm *llama) Predict(prompt string, fn func(string)) error {
|
||||
func (llm *llama) Predict(prompt string, fn func(api.GenerateResponse)) error {
|
||||
if tokens := llm.tokenize(prompt); tokens != nil {
|
||||
return llm.generate(tokens, fn)
|
||||
}
|
||||
@@ -169,7 +178,7 @@ func (llm *llama) detokenize(tokens ...C.llama_token) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (llm *llama) generate(tokens []C.llama_token, fn func(string)) error {
|
||||
func (llm *llama) generate(input []C.llama_token, fn func(api.GenerateResponse)) error {
|
||||
var opts C.struct_llama_sample_options
|
||||
opts.repeat_penalty = C.float(llm.RepeatPenalty)
|
||||
opts.frequency_penalty = C.float(llm.FrequencyPenalty)
|
||||
@@ -183,38 +192,58 @@ func (llm *llama) generate(tokens []C.llama_token, fn func(string)) error {
|
||||
opts.mirostat_tau = C.float(llm.MirostatTau)
|
||||
opts.mirostat_eta = C.float(llm.MirostatEta)
|
||||
|
||||
pastTokens := deque[C.llama_token]{capacity: llm.RepeatLastN}
|
||||
output := deque[C.llama_token]{capacity: llm.NumCtx}
|
||||
|
||||
for C.llama_get_kv_cache_token_count(llm.ctx) < C.int(llm.NumCtx) {
|
||||
if retval := C.llama_eval(llm.ctx, unsafe.SliceData(tokens), C.int(len(tokens)), C.llama_get_kv_cache_token_count(llm.ctx), C.int(llm.NumThread)); retval != 0 {
|
||||
if retval := C.llama_eval(llm.ctx, unsafe.SliceData(input), C.int(len(input)), C.llama_get_kv_cache_token_count(llm.ctx), C.int(llm.NumThread)); retval != 0 {
|
||||
return errors.New("llama: eval")
|
||||
}
|
||||
|
||||
token, err := llm.sample(pastTokens, &opts)
|
||||
switch {
|
||||
case err != nil:
|
||||
token, err := llm.sample(output, &opts)
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return err
|
||||
case errors.Is(err, io.EOF):
|
||||
return nil
|
||||
}
|
||||
|
||||
fn(llm.detokenize(token))
|
||||
// call the callback
|
||||
fn(api.GenerateResponse{
|
||||
Response: llm.detokenize(token),
|
||||
})
|
||||
|
||||
tokens = []C.llama_token{token}
|
||||
output.PushLeft(token)
|
||||
|
||||
pastTokens.PushLeft(token)
|
||||
input = []C.llama_token{token}
|
||||
}
|
||||
|
||||
dur := func(ms float64) time.Duration {
|
||||
d, err := time.ParseDuration(fmt.Sprintf("%fms", ms))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
timings := C.llama_get_timings(llm.ctx)
|
||||
fn(api.GenerateResponse{
|
||||
Done: true,
|
||||
PromptEvalCount: int(timings.n_p_eval),
|
||||
PromptEvalDuration: dur(float64(timings.t_p_eval_ms)),
|
||||
EvalCount: int(timings.n_eval),
|
||||
EvalDuration: dur(float64(timings.t_eval_ms)),
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (llm *llama) sample(pastTokens deque[C.llama_token], opts *C.struct_llama_sample_options) (C.llama_token, error) {
|
||||
func (llm *llama) sample(output deque[C.llama_token], opts *C.struct_llama_sample_options) (C.llama_token, error) {
|
||||
numVocab := int(C.llama_n_vocab(llm.ctx))
|
||||
logits := unsafe.Slice(C.llama_get_logits(llm.ctx), numVocab)
|
||||
|
||||
candidates := make([]C.struct_llama_token_data, 0, numVocab)
|
||||
for i := 0; i < numVocab; i++ {
|
||||
candidates = append(candidates, C.llama_token_data{
|
||||
candidates := deque[C.struct_llama_token_data]{capacity: numVocab}
|
||||
for i := 0; i < candidates.Cap(); i++ {
|
||||
candidates.PushLeft(C.struct_llama_token_data{
|
||||
id: C.int(i),
|
||||
logit: logits[i],
|
||||
p: 0,
|
||||
@@ -223,8 +252,8 @@ func (llm *llama) sample(pastTokens deque[C.llama_token], opts *C.struct_llama_s
|
||||
|
||||
token := C.llama_sample(
|
||||
llm.ctx,
|
||||
unsafe.SliceData(candidates), C.ulong(len(candidates)),
|
||||
unsafe.SliceData(pastTokens.Data()), C.ulong(pastTokens.Len()),
|
||||
unsafe.SliceData(candidates.Data()), C.ulong(candidates.Len()),
|
||||
unsafe.SliceData(output.Data()), C.ulong(output.Len()),
|
||||
opts)
|
||||
if token != C.llama_token_eos() {
|
||||
return token, nil
|
||||
|
||||
18
models.json
18
models.json
@@ -25,14 +25,14 @@
|
||||
},
|
||||
{
|
||||
"name": "vicuna",
|
||||
"display_name": "Wizard Vicuna Uncensored",
|
||||
"parameters": "13B",
|
||||
"url": "https://huggingface.co/TheBloke/Wizard-Vicuna-13B-Uncensored-GGML/resolve/main/Wizard-Vicuna-13B-Uncensored.ggmlv3.q2_K.bin",
|
||||
"short_description": "An uncensored model with no guardrails.",
|
||||
"description": "This model is trained with a subset of the dataset - responses that contained alignment / moralizing were removed. The intent is to train a WizardLM that doesn't have alignment built-in, so that alignment (of any sort) can be added separately with for example with a RLHF LoRA.",
|
||||
"display_name": "Vicuna",
|
||||
"parameters": "7B",
|
||||
"url": "https://huggingface.co/TheBloke/vicuna-7B-v1.3-GGML/resolve/main/vicuna-7b-v1.3.ggmlv3.q4_0.bin",
|
||||
"short_description": "Vicuna is a chat assistant trained by fine-tuning LLaMA on user-shared conversations collected from ShareGPT.",
|
||||
"description": "The primary use of Vicuna is research on large language models and chatbots. The primary intended users of the model are researchers and hobbyists in natural language processing, machine learning, and artificial intelligence.",
|
||||
"published_by": "TheBloke",
|
||||
"original_author": "ehartford",
|
||||
"original_url": "https://huggingface.co/ehartford/Wizard-Vicuna-13B-Uncensored",
|
||||
"license:": "GPL"
|
||||
"original_author": "LMSYS",
|
||||
"original_url": "https://huggingface.co/lmsys/vicuna-7b-v1.3",
|
||||
"license:": "Non-commercial"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -12,13 +12,15 @@ ARCH=$(go env GOARCH)
|
||||
|
||||
go build .
|
||||
|
||||
npm --prefix app run make:sign
|
||||
|
||||
# Create a new tag if it doesn't exist.
|
||||
if ! git rev-parse v$VERSION >/dev/null 2>&1; then
|
||||
git tag v$VERSION
|
||||
git push origin v$VERSION
|
||||
fi
|
||||
|
||||
mkdir dist
|
||||
mkdir -p dist
|
||||
cp app/out/make/zip/${OS}/${ARCH}/Ollama-${OS}-${ARCH}-${VERSION}.zip dist/Ollama-${OS}-${ARCH}.zip
|
||||
cp ./ollama dist/ollama-${OS}-${ARCH}
|
||||
|
||||
|
||||
@@ -76,22 +76,10 @@ func saveModel(model *Model, fn func(total, completed int64)) error {
|
||||
return fmt.Errorf("failed to download model: %w", err)
|
||||
}
|
||||
|
||||
// check if completed file exists
|
||||
fi, err := os.Stat(model.FullName())
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
// noop, file doesn't exist so create it
|
||||
case err != nil:
|
||||
return fmt.Errorf("stat: %w", err)
|
||||
default:
|
||||
fn(fi.Size(), fi.Size())
|
||||
return nil
|
||||
}
|
||||
|
||||
var size int64
|
||||
|
||||
// completed file doesn't exist, check partial file
|
||||
fi, err = os.Stat(model.TempFile())
|
||||
fi, err := os.Stat(model.TempFile())
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
// noop, file doesn't exist so create it
|
||||
@@ -119,25 +107,22 @@ func saveModel(model *Model, fn func(total, completed int64)) error {
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
totalSize, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
||||
remaining, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
||||
completed := size
|
||||
|
||||
totalBytes := size
|
||||
totalSize += size
|
||||
total := remaining + completed
|
||||
|
||||
for {
|
||||
n, err := io.CopyN(out, resp.Body, 8192)
|
||||
fn(total, completed)
|
||||
if completed >= total {
|
||||
return os.Rename(model.TempFile(), model.FullName())
|
||||
}
|
||||
|
||||
n , err := io.CopyN(out, resp.Body, 8192)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
totalBytes += n
|
||||
fn(totalSize, totalBytes)
|
||||
completed += n
|
||||
}
|
||||
|
||||
fn(totalSize, totalSize)
|
||||
return os.Rename(model.TempFile(), model.FullName())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"path"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
@@ -35,6 +36,8 @@ func cacheDir() string {
|
||||
}
|
||||
|
||||
func generate(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
req := api.GenerateRequest{
|
||||
Options: api.DefaultOptions(),
|
||||
}
|
||||
@@ -81,8 +84,14 @@ func generate(c *gin.Context) {
|
||||
}
|
||||
defer llm.Close()
|
||||
|
||||
fn := func(s string) {
|
||||
ch <- api.GenerateResponse{Response: s}
|
||||
fn := func(r api.GenerateResponse) {
|
||||
r.Model = req.Model
|
||||
r.CreatedAt = time.Now().UTC()
|
||||
if r.Done {
|
||||
r.TotalDuration = time.Since(start)
|
||||
}
|
||||
|
||||
ch <- r
|
||||
}
|
||||
|
||||
if err := llm.Predict(req.Prompt, fn); err != nil {
|
||||
@@ -105,6 +114,24 @@ func pull(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// check if completed file exists
|
||||
fi, err := os.Stat(remote.FullName())
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
// noop, file doesn't exist so create it
|
||||
case err != nil:
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
default:
|
||||
c.JSON(http.StatusOK, api.PullProgress{
|
||||
Total: fi.Size(),
|
||||
Completed: fi.Size(),
|
||||
Percent: 100,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ch := make(chan any)
|
||||
go stream(c, ch)
|
||||
|
||||
@@ -112,7 +139,7 @@ func pull(c *gin.Context) {
|
||||
ch <- api.PullProgress{
|
||||
Total: total,
|
||||
Completed: completed,
|
||||
Percent: float64(total) / float64(completed) * 100,
|
||||
Percent: float64(completed) / float64(total) * 100,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +156,7 @@ func Serve(ln net.Listener) error {
|
||||
c.String(http.StatusOK, "Ollama is running")
|
||||
})
|
||||
|
||||
r.POST("api/pull", pull)
|
||||
r.POST("/api/pull", pull)
|
||||
r.POST("/api/generate", generate)
|
||||
|
||||
log.Printf("Listening on %s", ln.Addr())
|
||||
|
||||
17
web/app/api/signup/route.ts
Normal file
17
web/app/api/signup/route.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Analytics } from '@segment/analytics-node'
|
||||
import { v4 as uuid } from 'uuid'
|
||||
|
||||
const analytics = new Analytics({ writeKey: process.env.TELEMETRY_WRITE_KEY || '<empty>' })
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { email } = await req.json()
|
||||
|
||||
analytics.identify({
|
||||
anonymousId: uuid(),
|
||||
traits: {
|
||||
email,
|
||||
},
|
||||
})
|
||||
|
||||
return new Response(null, { status: 200 })
|
||||
}
|
||||
11
web/app/download/downloader.tsx
Normal file
11
web/app/download/downloader.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
|
||||
export default function Downloader({ url }: { url: string }) {
|
||||
useEffect(() => {
|
||||
window.location.href = url
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,18 +1,19 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
import Downloader from './downloader'
|
||||
import Signup from './signup'
|
||||
|
||||
export default async function Download() {
|
||||
const res = await fetch('https://api.github.com/repos/jmorganca/ollama/releases', { next: { revalidate: 60 } })
|
||||
const data = await res.json()
|
||||
|
||||
if (data.length === 0) {
|
||||
return new Response('not found', { status: 404 })
|
||||
return null
|
||||
}
|
||||
|
||||
const latest = data[0]
|
||||
const assets = latest.assets || []
|
||||
|
||||
if (assets.length === 0) {
|
||||
return new Response('not found', { status: 404 })
|
||||
return null
|
||||
}
|
||||
|
||||
// todo: get the correct asset for the current arch/os
|
||||
@@ -21,12 +22,26 @@ export default async function Download() {
|
||||
)
|
||||
|
||||
if (!asset) {
|
||||
return new Response('not found', { status: 404 })
|
||||
return null
|
||||
}
|
||||
|
||||
if (asset) {
|
||||
redirect(asset.browser_download_url)
|
||||
}
|
||||
|
||||
return null
|
||||
return (
|
||||
<main className='flex min-h-screen max-w-2xl flex-col p-4 lg:p-24 items-center mx-auto'>
|
||||
<img src='/ollama.png' className='w-16 h-auto' />
|
||||
<section className='my-12 text-center'>
|
||||
<h2 className='my-2 max-w-md text-3xl tracking-tight'>Downloading Ollama</h2>
|
||||
<h3 className='text-sm text-neutral-500'>
|
||||
Problems downloading?{' '}
|
||||
<a href={asset.browser_download_url} className='underline'>
|
||||
Try again
|
||||
</a>
|
||||
</h3>
|
||||
<Downloader url={asset.browser_download_url} />
|
||||
</section>
|
||||
<section className='max-w-sm flex flex-col w-full items-center border border-neutral-200 rounded-xl px-8 pt-8 pb-2'>
|
||||
<p className='text-lg leading-tight text-center mb-6 max-w-[260px]'>Sign up for updates</p>
|
||||
<Signup />
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
51
web/app/download/signup.tsx
Normal file
51
web/app/download/signup.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Signup() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [success, setSuccess] = useState(false)
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={async e => {
|
||||
e.preventDefault()
|
||||
|
||||
setSubmitting(true)
|
||||
|
||||
await fetch('/api/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
|
||||
setSubmitting(false)
|
||||
setSuccess(true)
|
||||
setEmail('')
|
||||
|
||||
return false
|
||||
}}
|
||||
className='flex self-stretch flex-col gap-3 h-32'
|
||||
>
|
||||
<input
|
||||
required
|
||||
autoFocus
|
||||
value={email}
|
||||
onChange={e => setEmail(e.target.value)}
|
||||
type='email'
|
||||
placeholder='your@email.com'
|
||||
className='bg-neutral-100 rounded-lg px-4 py-2 focus:outline-none placeholder-neutral-500'
|
||||
/>
|
||||
<input
|
||||
type='submit'
|
||||
value='Get updates'
|
||||
disabled={submitting}
|
||||
className='bg-black text-white disabled:text-neutral-200 disabled:bg-neutral-700 rounded-lg px-4 py-2 focus:outline-none cursor-pointer'
|
||||
/>
|
||||
{success && <p className='text-center text-sm'>You're signed up for updates</p>}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ export const metadata = {
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang='en'>
|
||||
<body>{children}</body>
|
||||
<body className='antialiased'>{children}</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import models from '../../models.json'
|
||||
export default async function Home() {
|
||||
return (
|
||||
<main className='flex min-h-screen max-w-2xl flex-col p-4 lg:p-24'>
|
||||
<img src='/ollama.png' className='w-20 h-auto' />
|
||||
<img src='/ollama.png' className='w-16 h-auto' />
|
||||
<section className='my-4'>
|
||||
<p className='my-3 max-w-md'>
|
||||
<a className='underline' href='https://github.com/jmorganca/ollama'>
|
||||
@@ -14,7 +14,7 @@ export default async function Home() {
|
||||
is a tool for running large language models, currently for macOS with Windows and Linux coming soon.
|
||||
<br />
|
||||
<br />
|
||||
<a href='/download' target='_blank'>
|
||||
<a href='/download'>
|
||||
<button className='bg-black text-white text-sm py-2 px-3 rounded-lg flex items-center gap-2'>
|
||||
<AiFillApple className='h-auto w-5 relative -top-px' /> Download for macOS
|
||||
</button>
|
||||
|
||||
243
web/package-lock.json
generated
243
web/package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^19.0.13",
|
||||
"@octokit/types": "^11.0.0",
|
||||
"@segment/analytics-node": "^1.0.0",
|
||||
"@types/node": "20.4.0",
|
||||
"@types/react": "18.2.14",
|
||||
"@types/react-dom": "18.2.6",
|
||||
@@ -24,10 +25,14 @@
|
||||
"react-icons": "^4.10.1",
|
||||
"semver": "^7.5.3",
|
||||
"tailwindcss": "3.3.2",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.1.6",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/semver": "^7.5.0"
|
||||
"@types/semver": "^7.5.0",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
@@ -190,6 +195,25 @@
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||
},
|
||||
"node_modules/@lukeed/csprng": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
|
||||
"integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@lukeed/uuid": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/uuid/-/uuid-2.0.1.tgz",
|
||||
"integrity": "sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==",
|
||||
"dependencies": {
|
||||
"@lukeed/csprng": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "13.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.9.tgz",
|
||||
@@ -599,6 +623,31 @@
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.2.tgz",
|
||||
"integrity": "sha512-V+MvGwaHH03hYhY+k6Ef/xKd6RYlc4q8WBx+2ANmipHJcKuktNcI/NgEsJgdSUF6Lw32njT6OnrRsKYCdgHjYw=="
|
||||
},
|
||||
"node_modules/@segment/analytics-core": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@segment/analytics-core/-/analytics-core-1.3.0.tgz",
|
||||
"integrity": "sha512-ujScWZH49NK1hYlp2/EMw45nOPEh+pmTydAnR6gSkRNucZD4fuinvpPL03rmFCw8ibaMuKLAdgPJfQ0gkLKZ5A==",
|
||||
"dependencies": {
|
||||
"@lukeed/uuid": "^2.0.0",
|
||||
"dset": "^3.1.2",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@segment/analytics-node": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@segment/analytics-node/-/analytics-node-1.0.0.tgz",
|
||||
"integrity": "sha512-UWFujSxRkRauZuMVF4MPOT5QPvX4i7kiC2QCsozHhltoTiR2SBWRI86cYO/JI/Uk7qKaOxxGFDkJarCyIP7uLA==",
|
||||
"dependencies": {
|
||||
"@lukeed/uuid": "^2.0.0",
|
||||
"@segment/analytics-core": "1.3.0",
|
||||
"buffer": "^6.0.3",
|
||||
"node-fetch": "^2.6.7",
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz",
|
||||
@@ -651,6 +700,12 @@
|
||||
"integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz",
|
||||
"integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "5.60.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz",
|
||||
@@ -991,6 +1046,25 @@
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/before-after-hook": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
|
||||
@@ -1074,6 +1148,29 @@
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bundle-name": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz",
|
||||
@@ -1390,6 +1487,14 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dset": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz",
|
||||
"integrity": "sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.447",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.447.tgz",
|
||||
@@ -1747,9 +1852,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@@ -1784,9 +1889,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jsx-a11y/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@@ -1858,9 +1963,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
@@ -2385,6 +2490,25 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
|
||||
@@ -3521,6 +3645,95 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
|
||||
"integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-plugin-tailwindcss": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.4.0.tgz",
|
||||
"integrity": "sha512-Rna0sDPETA0KNhMHlN8wxKNgfSa8mTl2hPPAGxnbv6tUcHT6J4RQmQ8TLXyhB7Dm5Von4iHloBxTyClYM6wT0A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "*",
|
||||
"@prettier/plugin-pug": "*",
|
||||
"@shopify/prettier-plugin-liquid": "*",
|
||||
"@shufo/prettier-plugin-blade": "*",
|
||||
"@trivago/prettier-plugin-sort-imports": "*",
|
||||
"prettier": "^2.2 || ^3.0",
|
||||
"prettier-plugin-astro": "*",
|
||||
"prettier-plugin-css-order": "*",
|
||||
"prettier-plugin-import-sort": "*",
|
||||
"prettier-plugin-jsdoc": "*",
|
||||
"prettier-plugin-marko": "*",
|
||||
"prettier-plugin-organize-attributes": "*",
|
||||
"prettier-plugin-organize-imports": "*",
|
||||
"prettier-plugin-style-order": "*",
|
||||
"prettier-plugin-svelte": "*",
|
||||
"prettier-plugin-twig-melody": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@ianvs/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"@prettier/plugin-pug": {
|
||||
"optional": true
|
||||
},
|
||||
"@shopify/prettier-plugin-liquid": {
|
||||
"optional": true
|
||||
},
|
||||
"@shufo/prettier-plugin-blade": {
|
||||
"optional": true
|
||||
},
|
||||
"@trivago/prettier-plugin-sort-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-astro": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-css-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-import-sort": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-jsdoc": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-marko": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-attributes": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-organize-imports": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-style-order": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-svelte": {
|
||||
"optional": true
|
||||
},
|
||||
"prettier-plugin-twig-melody": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -4360,6 +4573,14 @@
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^19.0.13",
|
||||
"@octokit/types": "^11.0.0",
|
||||
"@segment/analytics-node": "^1.0.0",
|
||||
"@types/node": "20.4.0",
|
||||
"@types/react": "18.2.14",
|
||||
"@types/react-dom": "18.2.6",
|
||||
@@ -24,9 +25,13 @@
|
||||
"react-icons": "^4.10.1",
|
||||
"semver": "^7.5.3",
|
||||
"tailwindcss": "3.3.2",
|
||||
"typescript": "5.1.6"
|
||||
"typescript": "5.1.6",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/semver": "^7.5.0"
|
||||
"@types/semver": "^7.5.0",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"prettier": "^3.0.0",
|
||||
"prettier-plugin-tailwindcss": "^0.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user