Skip to content
Merged

UI #2

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,31 @@ jobs:
--clobber
env:
GH_TOKEN: ${{ github.token }}

docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: tanq16
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: tanq16/ai-context:main

publish:
needs: [process-commit, build]
Expand Down
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM golang:alpine AS builder
WORKDIR /app
COPY . .
RUN go build -ldflags="-s -w" -o ai-context .

FROM alpine:latest
WORKDIR /app
RUN mkdir -p /app/context
COPY --from=builder /app/ai-context .
EXPOSE 8080
CMD ["/app/ai-context"]
17 changes: 14 additions & 3 deletions aicontext/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,33 @@ func GetOutFileName(input string) string {
res := strings.ToLower(re.ReplaceAllString(input, "_"))
res = reReplace.ReplaceAllString(res, "")
res = strings.Trim(res, "_")
// return res + "-" + time.Now().Format("150405") + ".md"
return res + ".md"
}

func cleanURL(rawURL string) (string, error) {
if after, ok := strings.CutPrefix(rawURL, "github/"); ok {
rawURL = "https://github.com/" + after
}
if match, _ := regexp.MatchString(URLRegex["dir"], rawURL); match {
if match, _ := regexp.MatchString(`^\.?\.?\/.*`, rawURL); match {
return rawURL, nil
}
parsedURL, err := url.Parse(rawURL)
if err != nil {
return "", fmt.Errorf("failed to parse url: %w", err)
}
parsedURL.RawQuery = ""
// preserve video id for yt
if strings.Contains(parsedURL.Host, "youtube.com") {
videoID := parsedURL.Query().Get("v")
if videoID != "" {
query := url.Values{}
query.Set("v", videoID)
parsedURL.RawQuery = query.Encode()
} else {
parsedURL.RawQuery = ""
}
} else {
parsedURL.RawQuery = ""
}
parsedURL.Fragment = ""
return parsedURL.String(), nil
}
Expand Down
182 changes: 182 additions & 0 deletions cmd/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package cmd

import (
"embed"
"encoding/json"
"fmt"
"io/fs"
"log"
"net/http"
"os"
"path/filepath"

"github.com/spf13/cobra"
"github.com/tanq16/ai-context/aicontext"
)

//go:embed all:web
var webFS embed.FS

var serveCmd = &cobra.Command{
Use: "serve",
Short: "Launch a web server to use the AI Context tool through a UI.",
Run: runServer,
}

type generateRequest struct {
URL string `json:"url"`
Ignore []string `json:"ignore"`
}

type generateResponse struct {
Content string `json:"content"`
}

func runServer(cmd *cobra.Command, args []string) {
staticFS, err := fs.Sub(webFS, "web")
if err != nil {
log.Fatalf("Failed to create static file system: %v", err)
}

http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
http.HandleFunc("/generate", generateHandler)
http.HandleFunc("/load", loadHandler)
http.HandleFunc("/clear", clearHandler) // Add the new /clear route
http.HandleFunc("/", rootHandler(staticFS))

port := "8080"
fmt.Printf("Starting server at http://localhost:%s\n", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}

func rootHandler(fs fs.FS) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.FileServer(http.FS(fs)).ServeHTTP(w, r)
return
}
indexHTML, err := webFS.ReadFile("web/index.html")
if err != nil {
http.Error(w, "Could not read index.html", http.StatusInternalServerError)
log.Printf("Error reading embedded index.html: %v", err)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(indexHTML)
}
}

// clearHandler handles the request to delete the generated context file.
func clearHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}

if err := cleanupContextDir(); err != nil {
log.Printf("Error during cleanup: %v", err)
http.Error(w, "Failed to clear context file", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusNoContent) // Success, no content to return
}

func loadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Only GET method is allowed", http.StatusMethodNotAllowed)
return
}

outputFile, err := findGeneratedFile()
if err != nil {
json.NewEncoder(w).Encode(generateResponse{Content: ""})
return
}

content, err := os.ReadFile(outputFile)
if err != nil {
http.Error(w, "Failed to read context file", http.StatusInternalServerError)
log.Printf("Error reading output file %s: %v", outputFile, err)
return
}

resp := generateResponse{Content: string(content)}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}

func generateHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Only POST method is allowed", http.StatusMethodNotAllowed)
return
}

var req generateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if req.URL == "" {
http.Error(w, "URL is required", http.StatusBadRequest)
return
}

if err := cleanupContextDir(); err != nil {
log.Printf("Warning: could not clean up context directory: %v", err)
}

aicontext.Handler([]string{req.URL}, req.Ignore, 1)

outputFile, err := findGeneratedFile()
if err != nil {
http.Error(w, "Failed to find generated context file", http.StatusInternalServerError)
log.Printf("Error finding generated file: %v", err)
return
}

content, err := os.ReadFile(outputFile)
if err != nil {
http.Error(w, "Failed to read context file", http.StatusInternalServerError)
log.Printf("Error reading output file %s: %v", outputFile, err)
return
}

resp := generateResponse{Content: string(content)}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
log.Printf("Error encoding response: %v", err)
}
}

func cleanupContextDir() error {
dir := "context"
files, err := filepath.Glob(filepath.Join(dir, "*.md"))
if err != nil {
return err
}
for _, file := range files {
if err := os.Remove(file); err != nil {
log.Printf("Failed to remove file %s: %v", file, err)
}
}
return nil
}

func findGeneratedFile() (string, error) {
dir := "context"
files, err := filepath.Glob(filepath.Join(dir, "*.md"))
if err != nil {
return "", fmt.Errorf("error searching for files: %w", err)
}
if len(files) == 0 {
return "", fmt.Errorf("no markdown file found in context directory")
}
return files[0], nil
}

func init() {
rootCmd.AddCommand(serveCmd)
}
39 changes: 39 additions & 0 deletions cmd/web/asset-download.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

mkdir -p static/css
mkdir -p static/js
mkdir -p static/webfonts
mkdir -p static/fonts

echo "Downloading assets..."

# Download Tailwind CSS
curl -sL "https://cdn.tailwindcss.com" -o "static/js/tailwindcss.js"

# Download Font Awesome CSS and its webfonts
curl -sL "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" -o "static/css/all.min.css"
curl -sL "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-brands-400.woff2" -o "static/webfonts/fa-brands-400.woff2"
curl -sL "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-regular-400.woff2" -o "static/webfonts/fa-regular-400.woff2"
curl -sL "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-solid-900.woff2" -o "static/webfonts/fa-solid-900.woff2"
curl -sL "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-v4compatibility.woff2" -o "static/webfonts/fa-v4compatibility.woff2"

# Update Font Awesome CSS to use local webfonts path
sed -i.bak 's|https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/|/static/webfonts/|g' static/css/all.min.css
rm static/css/all.min.css.bak

# Download Inter font CSS from Google Fonts
curl -sL "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" -A "Mozilla/5.0" -o "static/css/inter.css"

# Download font files referenced in the CSS
grep -o 'https://fonts.gstatic.com/s/inter/[^)]*' static/css/inter.css | while read -r url; do
# Remove the single quote from the end
clean_url=$(echo "$url" | sed "s/'$//")
filename=$(basename "$clean_url")
curl -sL "$clean_url" -o "static/fonts/$filename"
done

# Update font CSS to use local font files
sed -i.bak 's|https://fonts.gstatic.com/s/inter/v[0-9]*/|/static/fonts/|g' static/css/inter.css
rm static/css/inter.css.bak

echo "All assets downloaded successfully!"
Loading