diff --git a/main.go b/main.go index b018995..96e1bfc 100644 --- a/main.go +++ b/main.go @@ -11,7 +11,7 @@ import ( "time" ) -// Message represents a single message in the conversation. +// Message represents one message in the conversation. type Message struct { Role string `json:"role"` Content string `json:"content"` @@ -21,15 +21,14 @@ type Message struct { type ChatCompletionRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` - // no temperature field for "o3-mini" } -// ChatCompletionResponseChoice represents one of the returned choices. +// ChatCompletionResponseChoice is a single reply (choice) from the API. type ChatCompletionResponseChoice struct { Message Message `json:"message"` } -// ChatCompletionResponse is the API response structure. +// ChatCompletionResponse represents the API response. type ChatCompletionResponse struct { Choices []ChatCompletionResponseChoice `json:"choices"` Error *struct { @@ -48,35 +47,30 @@ func init() { if openaiAPIKey == "" { log.Fatal("Environment variable OPENAI_API_KEY is not set") } - + // Parse the HTML template with responsive layout updates. indexTmpl = template.Must(template.New("index").Parse(indexHTML)) } func main() { http.HandleFunc("/", indexHandler) http.HandleFunc("/chat", chatHandler) - http.HandleFunc("/title", titleHandler) // New endpoint for titling conversation - + http.HandleFunc("/title", titleHandler) addr := ":8080" log.Println("Server starting on", addr) log.Fatal(http.ListenAndServe(addr, nil)) } -// indexHandler serves the main HTML page. func indexHandler(w http.ResponseWriter, r *http.Request) { if err := indexTmpl.Execute(w, nil); err != nil { http.Error(w, "Unable to load page", http.StatusInternalServerError) } } -// chatHandler receives conversation messages from the client, -// calls the OpenAI API, and returns the assistant's reply. func chatHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) return } - var clientRequest struct { Messages []Message `json:"messages"` } @@ -84,18 +78,15 @@ func chatHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) return } - apiRequestData := ChatCompletionRequest{ Model: "o3-mini", Messages: clientRequest.Messages, } - requestBytes, err := json.Marshal(apiRequestData) if err != nil { http.Error(w, "Error marshalling request: "+err.Error(), http.StatusInternalServerError) return } - client := http.Client{Timeout: 60 * time.Second} req, err := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(requestBytes)) if err != nil { @@ -104,54 +95,42 @@ func chatHandler(w http.ResponseWriter, r *http.Request) { } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+openaiAPIKey) - apiResponse, err := client.Do(req) if err != nil { http.Error(w, "Error calling OpenAI API: "+err.Error(), http.StatusInternalServerError) return } defer apiResponse.Body.Close() - bodyBytes, err := io.ReadAll(apiResponse.Body) if err != nil { http.Error(w, "Error reading API response: "+err.Error(), http.StatusInternalServerError) return } - var completionResponse ChatCompletionResponse if err := json.Unmarshal(bodyBytes, &completionResponse); err != nil { http.Error(w, "Error parsing API response: "+err.Error(), http.StatusInternalServerError) return } - if completionResponse.Error != nil { http.Error(w, "OpenAI API error: "+completionResponse.Error.Message, http.StatusInternalServerError) return } - if len(completionResponse.Choices) == 0 { http.Error(w, "No choices returned from OpenAI API", http.StatusInternalServerError) return } - responsePayload := map[string]string{ "reply": completionResponse.Choices[0].Message.Content, } - w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(responsePayload) } -// titleHandler provides an automatic title for the conversation. -// It expects a POST with JSON: { messages: []Message } and then -// calls the OpenAI API with a special prompt to generate a title. func titleHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed) return } - - // Read the incoming JSON containing the conversation messages. var clientRequest struct { Messages []Message `json:"messages"` } @@ -159,28 +138,22 @@ func titleHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) return } - - // Prepend a system message that instructs the AI to generate - // a short title based on the conversation. titleRequestMessages := []Message{ { Role: "system", - Content: "You are an AI that generates conversation titles. Given the conversation below, provide a short, descriptive title (in no more than five words) with no additional commentary.", + Content: "You are an AI that generates conversation titles. Based on the following conversation, please provide a short, descriptive title of no more than five words with no extra commentary.", }, } titleRequestMessages = append(titleRequestMessages, clientRequest.Messages...) - apiRequestData := ChatCompletionRequest{ Model: "o3-mini", Messages: titleRequestMessages, } - requestBytes, err := json.Marshal(apiRequestData) if err != nil { http.Error(w, "Error marshalling title request: "+err.Error(), http.StatusInternalServerError) return } - client := http.Client{Timeout: 30 * time.Second} req, err := http.NewRequest("POST", "https://api.openai.com/v1/chat/completions", bytes.NewBuffer(requestBytes)) if err != nil { @@ -189,40 +162,33 @@ func titleHandler(w http.ResponseWriter, r *http.Request) { } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+openaiAPIKey) - apiResponse, err := client.Do(req) if err != nil { http.Error(w, "Error calling OpenAI API for title: "+err.Error(), http.StatusInternalServerError) return } defer apiResponse.Body.Close() - bodyBytes, err := io.ReadAll(apiResponse.Body) if err != nil { http.Error(w, "Error reading title API response: "+err.Error(), http.StatusInternalServerError) return } - var completionResponse ChatCompletionResponse if err := json.Unmarshal(bodyBytes, &completionResponse); err != nil { http.Error(w, "Error parsing title API response: "+err.Error(), http.StatusInternalServerError) return } - if completionResponse.Error != nil { http.Error(w, "OpenAI API title error: "+completionResponse.Error.Message, http.StatusInternalServerError) return } - if len(completionResponse.Choices) == 0 { http.Error(w, "No title returned from OpenAI API", http.StatusInternalServerError) return } - responsePayload := map[string]string{ "title": completionResponse.Choices[0].Message.Content, } - w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(responsePayload) } @@ -234,82 +200,151 @@ const indexHTML = `