Phase 2-6: Git sync, sharing, 2FA, AI integration
- Git: init, commit, log, diff, restore, remotes, push/pull - Auto-commit on every file save - Sharing: share/unshare files with other users (ro/rw) - Shared documents view in sidebar - 2FA: TOTP setup/verify/disable, enforced at login - AI: verify spec endpoint (LiteLLM), generate (summarize/prompt/expand) - Light/dark theme with CSS variables - File delete (recursive for folders) - Admin panel + preferences panel - File creation timestamp display
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"markdownhub/internal/files"
|
||||
)
|
||||
|
||||
func (s *Server) handleAIVerify(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
if err := decodeBody(r, &req); err != nil || req.Path == "" {
|
||||
writeJSON(w, 400, map[string]string{"error": "path required"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := getUserID(r)
|
||||
content, err := files.ReadFile(s.dataDir, userID, req.Path)
|
||||
if err != nil {
|
||||
writeJSON(w, 404, map[string]string{"error": "file not found"})
|
||||
return
|
||||
}
|
||||
|
||||
aiEndpoint := os.Getenv("MH_AI_ENDPOINT")
|
||||
aiKey := os.Getenv("MH_AI_API_KEY")
|
||||
aiModel := os.Getenv("MH_AI_MODEL")
|
||||
if aiEndpoint == "" {
|
||||
writeJSON(w, 500, map[string]string{"error": "AI endpoint not configured (MH_AI_ENDPOINT)"})
|
||||
return
|
||||
}
|
||||
if aiModel == "" {
|
||||
aiModel = "gpt-4"
|
||||
}
|
||||
|
||||
// Call LiteLLM-compatible endpoint
|
||||
systemPrompt := `You are a technical reviewer. Review the following specification document for:
|
||||
1. Completeness - are there missing details needed to implement this?
|
||||
2. Ambiguities - are there unclear requirements?
|
||||
3. Feasibility - is this technically achievable?
|
||||
4. Suggestions - any improvements?
|
||||
|
||||
Respond with a structured review. End with a clear verdict: READY TO BUILD or NEEDS REVISION.`
|
||||
|
||||
response, err := callLLM(aiEndpoint, aiKey, aiModel, systemPrompt, content)
|
||||
if err != nil {
|
||||
writeJSON(w, 500, map[string]string{"error": "AI call failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
ready := strings.Contains(strings.ToUpper(response), "READY TO BUILD")
|
||||
writeJSON(w, 200, map[string]interface{}{
|
||||
"feedback": response,
|
||||
"ready": ready,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleAIGenerate(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Path string `json:"path"`
|
||||
Selection string `json:"selection"`
|
||||
Action string `json:"action"`
|
||||
OutputFolder string `json:"output_folder"`
|
||||
}
|
||||
if err := decodeBody(r, &req); err != nil {
|
||||
writeJSON(w, 400, map[string]string{"error": "invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
userID := getUserID(r)
|
||||
var inputText string
|
||||
if req.Selection != "" {
|
||||
inputText = req.Selection
|
||||
} else if req.Path != "" {
|
||||
content, err := files.ReadFile(s.dataDir, userID, req.Path)
|
||||
if err != nil {
|
||||
writeJSON(w, 404, map[string]string{"error": "file not found"})
|
||||
return
|
||||
}
|
||||
inputText = content
|
||||
}
|
||||
|
||||
aiEndpoint := os.Getenv("MH_AI_ENDPOINT")
|
||||
aiKey := os.Getenv("MH_AI_API_KEY")
|
||||
aiModel := os.Getenv("MH_AI_MODEL")
|
||||
if aiEndpoint == "" {
|
||||
writeJSON(w, 500, map[string]string{"error": "AI endpoint not configured"})
|
||||
return
|
||||
}
|
||||
if aiModel == "" {
|
||||
aiModel = "gpt-4"
|
||||
}
|
||||
|
||||
systemPrompt := "You are a helpful assistant. Respond in markdown."
|
||||
switch req.Action {
|
||||
case "summarize":
|
||||
systemPrompt = "Summarize the following text concisely in markdown."
|
||||
case "prompt":
|
||||
systemPrompt = "Generate a detailed AI prompt based on the following specification. The prompt should instruct an AI coding agent to implement the project."
|
||||
case "expand":
|
||||
systemPrompt = "Expand on the following text with more detail, examples, and explanations. Respond in markdown."
|
||||
}
|
||||
|
||||
response, err := callLLM(aiEndpoint, aiKey, aiModel, systemPrompt, inputText)
|
||||
if err != nil {
|
||||
writeJSON(w, 500, map[string]string{"error": "AI call failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Optionally save to folder
|
||||
if req.OutputFolder != "" {
|
||||
filename := fmt.Sprintf("%s/%s-output.md", req.OutputFolder, req.Action)
|
||||
files.WriteFile(s.dataDir, userID, filename, response)
|
||||
}
|
||||
|
||||
writeJSON(w, 200, map[string]string{"output": response})
|
||||
}
|
||||
|
||||
func callLLM(endpoint, apiKey, model, systemPrompt, userContent string) (string, error) {
|
||||
payload := map[string]interface{}{
|
||||
"model": model,
|
||||
"messages": []map[string]string{
|
||||
{"role": "system", "content": systemPrompt},
|
||||
{"role": "user", "content": userContent},
|
||||
},
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
|
||||
url := strings.TrimRight(endpoint, "/") + "/chat/completions"
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if apiKey != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("LLM returned %d: %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Choices []struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(result.Choices) == 0 {
|
||||
return "", fmt.Errorf("no response from LLM")
|
||||
}
|
||||
return result.Choices[0].Message.Content, nil
|
||||
}
|
||||
Reference in New Issue
Block a user